mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-04 11:24:35 -05:00
add trackers support (#720)
* add trackers support * Cleanup Tracker Code * Add GraphQL support for Tracking * Fix lint and deprecation errors * remove password from logs * Fixes after merge * Disable tracking for now * More disabled --------- Co-authored-by: Syer10 <syer10@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package suwayomi.tachidesk.graphql.dataLoaders
|
||||
|
||||
import com.expediagroup.graphql.dataloader.KotlinDataLoader
|
||||
import org.dataloader.DataLoader
|
||||
import org.dataloader.DataLoaderFactory
|
||||
import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger
|
||||
import org.jetbrains.exposed.sql.addLogger
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.types.TrackRecordNodeList
|
||||
import suwayomi.tachidesk.graphql.types.TrackRecordNodeList.Companion.toNodeList
|
||||
import suwayomi.tachidesk.graphql.types.TrackRecordType
|
||||
import suwayomi.tachidesk.graphql.types.TrackerType
|
||||
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.toTrack
|
||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
|
||||
class TrackerDataLoader : KotlinDataLoader<Int, TrackerType> {
|
||||
override val dataLoaderName = "TrackerDataLoader"
|
||||
|
||||
override fun getDataLoader(): DataLoader<Int, TrackerType> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
future {
|
||||
ids.map { id ->
|
||||
TrackerManager.getTracker(id)?.let { TrackerType(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TrackRecordsForMangaIdDataLoader : KotlinDataLoader<Int, TrackRecordNodeList> {
|
||||
override val dataLoaderName = "TrackRecordsForMangaIdDataLoader"
|
||||
|
||||
override fun getDataLoader(): DataLoader<Int, TrackRecordNodeList> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
val trackRecordsByMangaId =
|
||||
TrackRecordTable.select { TrackRecordTable.mangaId inList ids }
|
||||
.map { TrackRecordType(it) }
|
||||
.groupBy { it.mangaId }
|
||||
ids.map { (trackRecordsByMangaId[it] ?: emptyList()).toNodeList() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DisplayScoreForTrackRecordDataLoader : KotlinDataLoader<Int, String> {
|
||||
override val dataLoaderName = "DisplayScoreForTrackRecordDataLoader"
|
||||
|
||||
override fun getDataLoader(): DataLoader<Int, String> =
|
||||
DataLoaderFactory.newDataLoader<Int, String> { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
val trackRecords =
|
||||
TrackRecordTable.select { TrackRecordTable.id inList ids }
|
||||
.toList()
|
||||
.map { it.toTrack() }
|
||||
.associateBy { it.id!! }
|
||||
.mapValues { TrackerManager.getTracker(it.value.sync_id)?.displayScore(it.value) }
|
||||
|
||||
ids.map { trackRecords[it] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TrackRecordsForTrackerIdDataLoader : KotlinDataLoader<Int, TrackRecordNodeList> {
|
||||
override val dataLoaderName = "TrackRecordsForTrackerIdDataLoader"
|
||||
|
||||
override fun getDataLoader(): DataLoader<Int, TrackRecordNodeList> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
val trackRecordsBySyncId =
|
||||
TrackRecordTable.select { TrackRecordTable.syncId inList ids }
|
||||
.map { TrackRecordType(it) }
|
||||
.groupBy { it.mangaId }
|
||||
ids.map { (trackRecordsBySyncId[it] ?: emptyList()).toNodeList() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TrackRecordDataLoader : KotlinDataLoader<Int, TrackRecordType> {
|
||||
override val dataLoaderName = "TrackRecordDataLoader"
|
||||
|
||||
override fun getDataLoader(): DataLoader<Int, TrackRecordType> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
val trackRecordsId =
|
||||
TrackRecordTable.select { TrackRecordTable.id inList ids }
|
||||
.map { TrackRecordType(it) }
|
||||
.associateBy { it.id }
|
||||
ids.map { trackRecordsId[it] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.types.TrackRecordType
|
||||
import suwayomi.tachidesk.graphql.types.TrackSearchType
|
||||
import suwayomi.tachidesk.graphql.types.TrackerType
|
||||
import suwayomi.tachidesk.manga.impl.track.Track
|
||||
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
||||
import suwayomi.tachidesk.manga.model.dataclass.TrackSearchDataClass
|
||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class TrackMutation {
|
||||
data class LoginTrackerOAuthInput(
|
||||
val clientMutationId: String? = null,
|
||||
val trackerId: Int,
|
||||
val callbackUrl: String,
|
||||
)
|
||||
|
||||
data class LoginTrackerOAuthPayload(
|
||||
val clientMutationId: String?,
|
||||
val isLoggedIn: Boolean,
|
||||
val tracker: TrackerType,
|
||||
)
|
||||
|
||||
fun loginTrackerOAuth(input: LoginTrackerOAuthInput): CompletableFuture<LoginTrackerOAuthPayload> {
|
||||
val tracker =
|
||||
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
|
||||
"Could not find tracker"
|
||||
}
|
||||
return future {
|
||||
tracker.authCallback(input.callbackUrl)
|
||||
val trackerType = TrackerType(tracker)
|
||||
LoginTrackerOAuthPayload(
|
||||
input.clientMutationId,
|
||||
trackerType.isLoggedIn,
|
||||
trackerType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class LoginTrackerCredentialsInput(
|
||||
val clientMutationId: String? = null,
|
||||
val trackerId: Int,
|
||||
val username: String,
|
||||
val password: String,
|
||||
)
|
||||
|
||||
data class LoginTrackerCredentialsPayload(
|
||||
val clientMutationId: String?,
|
||||
val isLoggedIn: Boolean,
|
||||
val tracker: TrackerType,
|
||||
)
|
||||
|
||||
fun loginTrackerCredentials(input: LoginTrackerCredentialsInput): CompletableFuture<LoginTrackerCredentialsPayload> {
|
||||
val tracker =
|
||||
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
|
||||
"Could not find tracker"
|
||||
}
|
||||
return future {
|
||||
tracker.login(input.username, input.password)
|
||||
val trackerType = TrackerType(tracker)
|
||||
LoginTrackerCredentialsPayload(
|
||||
input.clientMutationId,
|
||||
trackerType.isLoggedIn,
|
||||
trackerType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class LogoutTrackerInput(
|
||||
val clientMutationId: String? = null,
|
||||
val trackerId: Int,
|
||||
)
|
||||
|
||||
data class LogoutTrackerPayload(
|
||||
val clientMutationId: String?,
|
||||
val isLoggedIn: Boolean,
|
||||
val tracker: TrackerType,
|
||||
)
|
||||
|
||||
fun logoutTracker(input: LogoutTrackerInput): CompletableFuture<LogoutTrackerPayload> {
|
||||
val tracker =
|
||||
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
|
||||
"Could not find tracker"
|
||||
}
|
||||
require(tracker.isLoggedIn) {
|
||||
"Cannot logout of a tracker that is not logged-in"
|
||||
}
|
||||
return future {
|
||||
tracker.logout()
|
||||
val trackerType = TrackerType(tracker)
|
||||
LogoutTrackerPayload(
|
||||
input.clientMutationId,
|
||||
trackerType.isLoggedIn,
|
||||
trackerType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class BindTrackInput(
|
||||
val clientMutationId: String? = null,
|
||||
val mangaId: Int,
|
||||
val track: TrackSearchType,
|
||||
)
|
||||
|
||||
data class BindTrackPayload(
|
||||
val clientMutationId: String?,
|
||||
val trackRecord: TrackRecordType,
|
||||
)
|
||||
|
||||
fun bindTrack(input: BindTrackInput): CompletableFuture<BindTrackPayload> {
|
||||
val (clientMutationId, mangaId, track) = input
|
||||
|
||||
return future {
|
||||
Track.bind(
|
||||
mangaId,
|
||||
TrackSearchDataClass(
|
||||
syncId = track.syncId,
|
||||
mediaId = track.mediaId,
|
||||
title = track.title,
|
||||
totalChapters = track.totalChapters,
|
||||
trackingUrl = track.trackingUrl,
|
||||
coverUrl = track.coverUrl,
|
||||
summary = track.summary,
|
||||
publishingStatus = track.publishingStatus,
|
||||
publishingType = track.publishingType,
|
||||
startDate = track.startDate,
|
||||
),
|
||||
)
|
||||
val trackRecord =
|
||||
transaction {
|
||||
TrackRecordTable.select {
|
||||
TrackRecordTable.mangaId eq mangaId and (TrackRecordTable.syncId eq track.syncId)
|
||||
}.first()
|
||||
}
|
||||
BindTrackPayload(
|
||||
clientMutationId,
|
||||
TrackRecordType(trackRecord),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class UpdateTrackInput(
|
||||
val clientMutationId: String? = null,
|
||||
val recordId: Int,
|
||||
val status: Int? = null,
|
||||
val lastChapterRead: Double? = null,
|
||||
val scoreString: String? = null,
|
||||
val startDate: Long? = null,
|
||||
val finishDate: Long? = null,
|
||||
val unbind: Boolean? = null,
|
||||
)
|
||||
|
||||
data class UpdateTrackPayload(
|
||||
val clientMutationId: String?,
|
||||
val trackRecord: TrackRecordType?,
|
||||
)
|
||||
|
||||
fun updateTrack(input: UpdateTrackInput): CompletableFuture<UpdateTrackPayload> {
|
||||
return future {
|
||||
Track.update(
|
||||
Track.UpdateInput(
|
||||
input.recordId,
|
||||
input.status,
|
||||
input.lastChapterRead,
|
||||
input.scoreString,
|
||||
input.startDate,
|
||||
input.finishDate,
|
||||
input.unbind,
|
||||
),
|
||||
)
|
||||
|
||||
val trackRecord =
|
||||
transaction {
|
||||
TrackRecordTable.select {
|
||||
TrackRecordTable.id eq input.recordId
|
||||
}.firstOrNull()
|
||||
}
|
||||
UpdateTrackPayload(
|
||||
input.clientMutationId,
|
||||
trackRecord?.let { TrackRecordType(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
package suwayomi.tachidesk.graphql.queries
|
||||
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.less
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.DoubleFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
|
||||
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.applyOps
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.OrderBy
|
||||
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
||||
import suwayomi.tachidesk.graphql.server.primitives.QueryResults
|
||||
import suwayomi.tachidesk.graphql.server.primitives.applyBeforeAfter
|
||||
import suwayomi.tachidesk.graphql.server.primitives.greaterNotUnique
|
||||
import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
|
||||
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
||||
import suwayomi.tachidesk.graphql.types.TrackRecordNodeList
|
||||
import suwayomi.tachidesk.graphql.types.TrackRecordType
|
||||
import suwayomi.tachidesk.graphql.types.TrackSearchType
|
||||
import suwayomi.tachidesk.graphql.types.TrackerNodeList
|
||||
import suwayomi.tachidesk.graphql.types.TrackerType
|
||||
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class TrackQuery {
|
||||
fun tracker(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
id: Int,
|
||||
): CompletableFuture<TrackerType> {
|
||||
return dataFetchingEnvironment.getValueFromDataLoader<Int, TrackerType>("TrackerDataLoader", id)
|
||||
}
|
||||
|
||||
enum class TrackerOrderBy {
|
||||
ID,
|
||||
NAME,
|
||||
IS_LOGGED_IN,
|
||||
;
|
||||
|
||||
fun greater(
|
||||
tracker: TrackerType,
|
||||
cursor: Cursor,
|
||||
): Boolean {
|
||||
return when (this) {
|
||||
ID -> tracker.id > cursor.value.toInt()
|
||||
NAME -> tracker.name > cursor.value
|
||||
IS_LOGGED_IN -> {
|
||||
val value = cursor.value.substringAfter('-').toBooleanStrict()
|
||||
!value || tracker.isLoggedIn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun less(
|
||||
tracker: TrackerType,
|
||||
cursor: Cursor,
|
||||
): Boolean {
|
||||
return when (this) {
|
||||
ID -> tracker.id < cursor.value.toInt()
|
||||
NAME -> tracker.name < cursor.value
|
||||
IS_LOGGED_IN -> {
|
||||
val value = cursor.value.substringAfter('-').toBooleanStrict()
|
||||
value || !tracker.isLoggedIn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun asCursor(type: TrackerType): Cursor {
|
||||
val value =
|
||||
when (this) {
|
||||
ID -> type.id.toString()
|
||||
NAME -> type.name
|
||||
IS_LOGGED_IN -> type.id.toString() + "-" + type.isLoggedIn
|
||||
}
|
||||
return Cursor(value)
|
||||
}
|
||||
}
|
||||
|
||||
data class TrackerCondition(
|
||||
val id: Int? = null,
|
||||
val name: String? = null,
|
||||
val icon: String? = null,
|
||||
val isLoggedIn: Boolean? = null,
|
||||
)
|
||||
|
||||
data class TrackerFilter(
|
||||
val id: IntFilter? = null,
|
||||
val name: StringFilter? = null,
|
||||
val icon: StringFilter? = null,
|
||||
val isLoggedIn: BooleanFilter? = null,
|
||||
val authUrl: StringFilter? = null,
|
||||
val and: List<TrackerFilter>? = null,
|
||||
val or: List<TrackerFilter>? = null,
|
||||
val not: TrackerFilter? = null,
|
||||
)
|
||||
|
||||
fun trackers(
|
||||
condition: TrackerCondition? = null,
|
||||
orderBy: TrackerOrderBy? = null,
|
||||
orderByType: SortOrder? = null,
|
||||
before: Cursor? = null,
|
||||
after: Cursor? = null,
|
||||
first: Int? = null,
|
||||
last: Int? = null,
|
||||
offset: Int? = null,
|
||||
): TrackerNodeList {
|
||||
val (queryResults, resultsAsType) =
|
||||
run {
|
||||
var res = TrackerManager.services.map { TrackerType(it) }
|
||||
|
||||
if (condition != null) {
|
||||
res =
|
||||
res.filter { tracker ->
|
||||
(condition.id == null || (condition.id == tracker.id)) &&
|
||||
(condition.name == null || (condition.name == tracker.name)) &&
|
||||
(condition.icon == null || (condition.icon == tracker.icon)) &&
|
||||
(condition.isLoggedIn == null || (condition.isLoggedIn == tracker.isLoggedIn))
|
||||
}
|
||||
}
|
||||
|
||||
if (orderBy != null || (last != null || before != null)) {
|
||||
val orderType = orderByType.maybeSwap(last ?: before)
|
||||
|
||||
res =
|
||||
when (orderType) {
|
||||
SortOrder.DESC, SortOrder.DESC_NULLS_FIRST, SortOrder.DESC_NULLS_LAST ->
|
||||
when (orderBy) {
|
||||
TrackerOrderBy.ID, null -> res.sortedByDescending { it.id }
|
||||
TrackerOrderBy.NAME -> res.sortedByDescending { it.name }
|
||||
TrackerOrderBy.IS_LOGGED_IN -> res.sortedByDescending { it.isLoggedIn }
|
||||
}
|
||||
SortOrder.ASC, SortOrder.ASC_NULLS_FIRST, SortOrder.ASC_NULLS_LAST ->
|
||||
when (orderBy) {
|
||||
TrackerOrderBy.ID, null -> res.sortedBy { it.id }
|
||||
TrackerOrderBy.NAME -> res.sortedBy { it.name }
|
||||
TrackerOrderBy.IS_LOGGED_IN -> res.sortedBy { it.isLoggedIn }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val total = res.size
|
||||
val firstResult = res.firstOrNull()
|
||||
val lastResult = res.lastOrNull()
|
||||
|
||||
val realOrderBy = orderBy ?: TrackerOrderBy.ID
|
||||
if (after != null) {
|
||||
res =
|
||||
res.filter {
|
||||
when (orderByType) {
|
||||
SortOrder.DESC, SortOrder.DESC_NULLS_FIRST, SortOrder.DESC_NULLS_LAST -> realOrderBy.less(it, after)
|
||||
null, SortOrder.ASC, SortOrder.ASC_NULLS_FIRST, SortOrder.ASC_NULLS_LAST -> realOrderBy.greater(it, after)
|
||||
}
|
||||
}
|
||||
} else if (before != null) {
|
||||
res =
|
||||
res.filter {
|
||||
when (orderByType) {
|
||||
SortOrder.DESC, SortOrder.DESC_NULLS_FIRST, SortOrder.DESC_NULLS_LAST -> realOrderBy.greater(it, before)
|
||||
null, SortOrder.ASC, SortOrder.ASC_NULLS_FIRST, SortOrder.ASC_NULLS_LAST -> realOrderBy.less(it, before)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (first != null) {
|
||||
res = res.drop(offset ?: 0).take(first)
|
||||
} else if (last != null) {
|
||||
res = res.take(last)
|
||||
}
|
||||
|
||||
QueryResults(total.toLong(), firstResult, lastResult, emptyList()) to res
|
||||
}
|
||||
|
||||
val getAsCursor: (TrackerType) -> Cursor = (orderBy ?: TrackerOrderBy.ID)::asCursor
|
||||
|
||||
return TrackerNodeList(
|
||||
resultsAsType,
|
||||
if (resultsAsType.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
listOfNotNull(
|
||||
resultsAsType.firstOrNull()?.let {
|
||||
TrackerNodeList.TrackerEdge(
|
||||
getAsCursor(it),
|
||||
it,
|
||||
)
|
||||
},
|
||||
resultsAsType.lastOrNull()?.let {
|
||||
TrackerNodeList.TrackerEdge(
|
||||
getAsCursor(it),
|
||||
it,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
pageInfo =
|
||||
PageInfo(
|
||||
hasNextPage = queryResults.lastKey?.id != resultsAsType.lastOrNull()?.id,
|
||||
hasPreviousPage = queryResults.firstKey?.id != resultsAsType.firstOrNull()?.id,
|
||||
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
|
||||
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) },
|
||||
),
|
||||
totalCount = queryResults.total.toInt(),
|
||||
)
|
||||
}
|
||||
|
||||
fun trackRecord(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
id: Int,
|
||||
): CompletableFuture<TrackRecordType> {
|
||||
return dataFetchingEnvironment.getValueFromDataLoader<Int, TrackRecordType>("TrackRecordDataLoader", id)
|
||||
}
|
||||
|
||||
enum class TrackRecordOrderBy(override val column: Column<out Comparable<*>>) : OrderBy<TrackRecordType> {
|
||||
ID(TrackRecordTable.id),
|
||||
MANGA_ID(TrackRecordTable.mangaId),
|
||||
SYNC_ID(TrackRecordTable.syncId),
|
||||
REMOTE_ID(TrackRecordTable.remoteId),
|
||||
TITLE(TrackRecordTable.title),
|
||||
LAST_CHAPTER_READ(TrackRecordTable.lastChapterRead),
|
||||
TOTAL_CHAPTERS(TrackRecordTable.lastChapterRead),
|
||||
SCORE(TrackRecordTable.score),
|
||||
START_DATE(TrackRecordTable.startDate),
|
||||
FINISH_DATE(TrackRecordTable.finishDate),
|
||||
;
|
||||
|
||||
override fun greater(cursor: Cursor): Op<Boolean> {
|
||||
return when (this) {
|
||||
ID -> TrackRecordTable.id greater cursor.value.toInt()
|
||||
MANGA_ID -> greaterNotUnique(TrackRecordTable.mangaId, TrackRecordTable.id, cursor)
|
||||
SYNC_ID -> greaterNotUnique(TrackRecordTable.syncId, TrackRecordTable.id, cursor, String::toInt)
|
||||
REMOTE_ID -> greaterNotUnique(TrackRecordTable.remoteId, TrackRecordTable.id, cursor, String::toLong)
|
||||
TITLE -> greaterNotUnique(TrackRecordTable.title, TrackRecordTable.id, cursor, String::toString)
|
||||
LAST_CHAPTER_READ -> greaterNotUnique(TrackRecordTable.lastChapterRead, TrackRecordTable.id, cursor, String::toDouble)
|
||||
TOTAL_CHAPTERS -> greaterNotUnique(TrackRecordTable.totalChapters, TrackRecordTable.id, cursor, String::toInt)
|
||||
SCORE -> greaterNotUnique(TrackRecordTable.score, TrackRecordTable.id, cursor, String::toDouble)
|
||||
START_DATE -> greaterNotUnique(TrackRecordTable.startDate, TrackRecordTable.id, cursor, String::toLong)
|
||||
FINISH_DATE -> greaterNotUnique(TrackRecordTable.finishDate, TrackRecordTable.id, cursor, String::toLong)
|
||||
}
|
||||
}
|
||||
|
||||
override fun less(cursor: Cursor): Op<Boolean> {
|
||||
return when (this) {
|
||||
ID -> TrackRecordTable.id less cursor.value.toInt()
|
||||
MANGA_ID -> lessNotUnique(TrackRecordTable.mangaId, TrackRecordTable.id, cursor)
|
||||
SYNC_ID -> lessNotUnique(TrackRecordTable.syncId, TrackRecordTable.id, cursor, String::toInt)
|
||||
REMOTE_ID -> lessNotUnique(TrackRecordTable.remoteId, TrackRecordTable.id, cursor, String::toLong)
|
||||
TITLE -> lessNotUnique(TrackRecordTable.title, TrackRecordTable.id, cursor, String::toString)
|
||||
LAST_CHAPTER_READ -> lessNotUnique(TrackRecordTable.lastChapterRead, TrackRecordTable.id, cursor, String::toDouble)
|
||||
TOTAL_CHAPTERS -> lessNotUnique(TrackRecordTable.totalChapters, TrackRecordTable.id, cursor, String::toInt)
|
||||
SCORE -> lessNotUnique(TrackRecordTable.score, TrackRecordTable.id, cursor, String::toDouble)
|
||||
START_DATE -> lessNotUnique(TrackRecordTable.startDate, TrackRecordTable.id, cursor, String::toLong)
|
||||
FINISH_DATE -> lessNotUnique(TrackRecordTable.finishDate, TrackRecordTable.id, cursor, String::toLong)
|
||||
}
|
||||
}
|
||||
|
||||
override fun asCursor(type: TrackRecordType): Cursor {
|
||||
val value =
|
||||
when (this) {
|
||||
ID -> type.id.toString()
|
||||
MANGA_ID -> type.id.toString() + "-" + type.mangaId
|
||||
SYNC_ID -> type.id.toString() + "-" + type.syncId
|
||||
REMOTE_ID -> type.id.toString() + "-" + type.remoteId
|
||||
TITLE -> type.id.toString() + "-" + type.title
|
||||
LAST_CHAPTER_READ -> type.id.toString() + "-" + type.lastChapterRead
|
||||
TOTAL_CHAPTERS -> type.id.toString() + "-" + type.totalChapters
|
||||
SCORE -> type.id.toString() + "-" + type.score
|
||||
START_DATE -> type.id.toString() + "-" + type.startDate
|
||||
FINISH_DATE -> type.id.toString() + "-" + type.finishDate
|
||||
}
|
||||
return Cursor(value)
|
||||
}
|
||||
}
|
||||
|
||||
data class TrackRecordCondition(
|
||||
val id: Int? = null,
|
||||
val mangaId: Int? = null,
|
||||
val syncId: Int? = null,
|
||||
val remoteId: Long? = null,
|
||||
val libraryId: Long? = null,
|
||||
val title: String? = null,
|
||||
val lastChapterRead: Double? = null,
|
||||
val totalChapters: Int? = null,
|
||||
val status: Int? = null,
|
||||
val score: Double? = null,
|
||||
val remoteUrl: String? = null,
|
||||
val startDate: Long? = null,
|
||||
val finishDate: Long? = null,
|
||||
) : HasGetOp {
|
||||
override fun getOp(): Op<Boolean>? {
|
||||
val opAnd = OpAnd()
|
||||
opAnd.eq(id, TrackRecordTable.id)
|
||||
opAnd.eq(mangaId, TrackRecordTable.mangaId)
|
||||
opAnd.eq(syncId, TrackRecordTable.syncId)
|
||||
opAnd.eq(remoteId, TrackRecordTable.remoteId)
|
||||
opAnd.eq(libraryId, TrackRecordTable.libraryId)
|
||||
opAnd.eq(title, TrackRecordTable.title)
|
||||
opAnd.eq(lastChapterRead, TrackRecordTable.lastChapterRead)
|
||||
opAnd.eq(totalChapters, TrackRecordTable.totalChapters)
|
||||
opAnd.eq(status, TrackRecordTable.status)
|
||||
opAnd.eq(score, TrackRecordTable.score)
|
||||
opAnd.eq(remoteUrl, TrackRecordTable.remoteUrl)
|
||||
opAnd.eq(startDate, TrackRecordTable.startDate)
|
||||
opAnd.eq(finishDate, TrackRecordTable.finishDate)
|
||||
|
||||
return opAnd.op
|
||||
}
|
||||
}
|
||||
|
||||
data class TrackRecordFilter(
|
||||
val id: IntFilter? = null,
|
||||
val mangaId: IntFilter? = null,
|
||||
val syncId: IntFilter? = null,
|
||||
val remoteId: LongFilter? = null,
|
||||
val libraryId: LongFilter? = null,
|
||||
val title: StringFilter? = null,
|
||||
val lastChapterRead: DoubleFilter? = null,
|
||||
val totalChapters: IntFilter? = null,
|
||||
val status: IntFilter? = null,
|
||||
val score: DoubleFilter? = null,
|
||||
val remoteUrl: StringFilter? = null,
|
||||
val startDate: LongFilter? = null,
|
||||
val finishDate: LongFilter? = null,
|
||||
override val and: List<TrackRecordFilter>? = null,
|
||||
override val or: List<TrackRecordFilter>? = null,
|
||||
override val not: TrackRecordFilter? = null,
|
||||
) : Filter<TrackRecordFilter> {
|
||||
override fun getOpList(): List<Op<Boolean>> {
|
||||
return listOfNotNull(
|
||||
andFilterWithCompareEntity(TrackRecordTable.id, id),
|
||||
andFilterWithCompareEntity(TrackRecordTable.mangaId, mangaId),
|
||||
andFilterWithCompare(TrackRecordTable.syncId, syncId),
|
||||
andFilterWithCompare(TrackRecordTable.remoteId, remoteId),
|
||||
andFilterWithCompare(TrackRecordTable.libraryId, libraryId),
|
||||
andFilterWithCompareString(TrackRecordTable.title, title),
|
||||
andFilterWithCompare(TrackRecordTable.lastChapterRead, lastChapterRead),
|
||||
andFilterWithCompare(TrackRecordTable.totalChapters, totalChapters),
|
||||
andFilterWithCompare(TrackRecordTable.status, status),
|
||||
andFilterWithCompare(TrackRecordTable.score, score),
|
||||
andFilterWithCompareString(TrackRecordTable.remoteUrl, remoteUrl),
|
||||
andFilterWithCompare(TrackRecordTable.startDate, startDate),
|
||||
andFilterWithCompare(TrackRecordTable.finishDate, finishDate),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun trackRecords(
|
||||
condition: TrackRecordCondition? = null,
|
||||
filter: TrackRecordFilter? = null,
|
||||
orderBy: TrackRecordOrderBy? = null,
|
||||
orderByType: SortOrder? = null,
|
||||
before: Cursor? = null,
|
||||
after: Cursor? = null,
|
||||
first: Int? = null,
|
||||
last: Int? = null,
|
||||
offset: Int? = null,
|
||||
): TrackRecordNodeList {
|
||||
val queryResults =
|
||||
transaction {
|
||||
val res = TrackRecordTable.selectAll()
|
||||
|
||||
res.applyOps(condition, filter)
|
||||
|
||||
if (orderBy != null || (last != null || before != null)) {
|
||||
val orderByColumn = orderBy?.column ?: TrackRecordTable.id
|
||||
val orderType = orderByType.maybeSwap(last ?: before)
|
||||
|
||||
if (orderBy == TrackRecordOrderBy.ID || orderBy == null) {
|
||||
res.orderBy(orderByColumn to orderType)
|
||||
} else {
|
||||
res.orderBy(
|
||||
orderByColumn to orderType,
|
||||
TrackRecordTable.id to SortOrder.ASC,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val total = res.count()
|
||||
val firstResult = res.firstOrNull()?.get(TrackRecordTable.id)?.value
|
||||
val lastResult = res.lastOrNull()?.get(TrackRecordTable.id)?.value
|
||||
|
||||
res.applyBeforeAfter(
|
||||
before = before,
|
||||
after = after,
|
||||
orderBy = orderBy ?: TrackRecordOrderBy.ID,
|
||||
orderByType = orderByType,
|
||||
)
|
||||
|
||||
if (first != null) {
|
||||
res.limit(first, offset?.toLong() ?: 0)
|
||||
} else if (last != null) {
|
||||
res.limit(last)
|
||||
}
|
||||
|
||||
QueryResults(total, firstResult, lastResult, res.toList())
|
||||
}
|
||||
|
||||
val getAsCursor: (TrackRecordType) -> Cursor = (orderBy ?: TrackRecordOrderBy.ID)::asCursor
|
||||
|
||||
val resultsAsType = queryResults.results.map { TrackRecordType(it) }
|
||||
|
||||
return TrackRecordNodeList(
|
||||
resultsAsType,
|
||||
if (resultsAsType.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
listOfNotNull(
|
||||
resultsAsType.firstOrNull()?.let {
|
||||
TrackRecordNodeList.TrackRecordEdge(
|
||||
getAsCursor(it),
|
||||
it,
|
||||
)
|
||||
},
|
||||
resultsAsType.lastOrNull()?.let {
|
||||
TrackRecordNodeList.TrackRecordEdge(
|
||||
getAsCursor(it),
|
||||
it,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
pageInfo =
|
||||
PageInfo(
|
||||
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.id,
|
||||
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.id,
|
||||
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
|
||||
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) },
|
||||
),
|
||||
totalCount = queryResults.total.toInt(),
|
||||
)
|
||||
}
|
||||
|
||||
data class SearchTrackerInput(
|
||||
val trackerId: Int,
|
||||
val query: String,
|
||||
)
|
||||
|
||||
data class SearchTrackerPayload(val trackSearches: List<TrackSearchType>)
|
||||
|
||||
fun searchTracker(input: SearchTrackerInput): CompletableFuture<SearchTrackerPayload> {
|
||||
return future {
|
||||
val tracker =
|
||||
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
|
||||
"Tracker not found"
|
||||
}
|
||||
require(tracker.isLoggedIn) {
|
||||
"Tracker needs to be logged-in to search"
|
||||
}
|
||||
SearchTrackerPayload(
|
||||
tracker.search(input.query).map {
|
||||
TrackSearchType(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,6 +259,20 @@ data class FloatFilter(
|
||||
override val greaterThanOrEqualTo: Float? = null,
|
||||
) : ComparableScalarFilter<Float>
|
||||
|
||||
data class DoubleFilter(
|
||||
override val isNull: Boolean? = null,
|
||||
override val equalTo: Double? = null,
|
||||
override val notEqualTo: Double? = null,
|
||||
override val distinctFrom: Double? = null,
|
||||
override val notDistinctFrom: Double? = null,
|
||||
override val `in`: List<Double>? = null,
|
||||
override val notIn: List<Double>? = null,
|
||||
override val lessThan: Double? = null,
|
||||
override val lessThanOrEqualTo: Double? = null,
|
||||
override val greaterThan: Double? = null,
|
||||
override val greaterThanOrEqualTo: Double? = null,
|
||||
) : ComparableScalarFilter<Double>
|
||||
|
||||
data class StringFilter(
|
||||
override val isNull: Boolean? = null,
|
||||
override val equalTo: String? = null,
|
||||
@@ -418,8 +432,8 @@ class OpAnd(var op: Op<Boolean>? = null) {
|
||||
) = andWhere(value) { column like it }
|
||||
}
|
||||
|
||||
fun <T : Comparable<T>> andFilterWithCompare(
|
||||
column: Column<T>,
|
||||
fun <T : Comparable<T>, S : T?> andFilterWithCompare(
|
||||
column: Column<S>,
|
||||
filter: ComparableScalarFilter<T>?,
|
||||
): Op<Boolean>? {
|
||||
filter ?: return null
|
||||
@@ -448,23 +462,24 @@ fun <T : Comparable<T>> andFilterWithCompareEntity(
|
||||
return opAnd.op
|
||||
}
|
||||
|
||||
fun <T : Comparable<T>> andFilter(
|
||||
column: Column<T>,
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : Comparable<T>, S : T?> andFilter(
|
||||
column: Column<S>,
|
||||
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) }
|
||||
opAnd.andWhere(filter.equalTo) { column eq it as S }
|
||||
opAnd.andWhere(filter.notEqualTo) { column neq it as S }
|
||||
opAnd.andWhere(filter.distinctFrom) { DistinctFromOp.distinctFrom(column, it as S) }
|
||||
opAnd.andWhere(filter.notDistinctFrom) { DistinctFromOp.notDistinctFrom(column, it as S) }
|
||||
if (!filter.`in`.isNullOrEmpty()) {
|
||||
opAnd.andWhere(filter.`in`) { column inList it }
|
||||
opAnd.andWhere(filter.`in`) { column inList it as List<S> }
|
||||
}
|
||||
if (!filter.notIn.isNullOrEmpty()) {
|
||||
opAnd.andWhere(filter.notIn) { column notInList it }
|
||||
opAnd.andWhere(filter.notIn) { column notInList it as List<S> }
|
||||
}
|
||||
return opAnd.op
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import suwayomi.tachidesk.graphql.dataLoaders.CategoryMetaDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.ChapterDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.ChapterMetaDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.ChaptersForMangaDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.DisplayScoreForTrackRecordDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.DownloadedChapterCountForMangaDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForSourceDataLoader
|
||||
@@ -27,6 +28,10 @@ import suwayomi.tachidesk.graphql.dataLoaders.MangaForSourceDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.MangaMetaDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.SourceDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.SourcesForExtensionDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.TrackRecordDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.TrackRecordsForMangaIdDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.TrackRecordsForTrackerIdDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.TrackerDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.UnreadChapterCountForMangaDataLoader
|
||||
|
||||
class TachideskDataLoaderRegistryFactory {
|
||||
@@ -53,6 +58,11 @@ class TachideskDataLoaderRegistryFactory {
|
||||
SourcesForExtensionDataLoader(),
|
||||
ExtensionDataLoader(),
|
||||
ExtensionForSourceDataLoader(),
|
||||
// TrackerDataLoader(),
|
||||
// TrackRecordsForMangaIdDataLoader(),
|
||||
// DisplayScoreForTrackRecordDataLoader(),
|
||||
// TrackRecordsForTrackerIdDataLoader(),
|
||||
// TrackRecordDataLoader(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import suwayomi.tachidesk.graphql.mutations.MangaMutation
|
||||
import suwayomi.tachidesk.graphql.mutations.MetaMutation
|
||||
import suwayomi.tachidesk.graphql.mutations.SettingsMutation
|
||||
import suwayomi.tachidesk.graphql.mutations.SourceMutation
|
||||
import suwayomi.tachidesk.graphql.mutations.TrackMutation
|
||||
import suwayomi.tachidesk.graphql.mutations.UpdateMutation
|
||||
import suwayomi.tachidesk.graphql.queries.BackupQuery
|
||||
import suwayomi.tachidesk.graphql.queries.CategoryQuery
|
||||
@@ -35,6 +36,7 @@ import suwayomi.tachidesk.graphql.queries.MangaQuery
|
||||
import suwayomi.tachidesk.graphql.queries.MetaQuery
|
||||
import suwayomi.tachidesk.graphql.queries.SettingsQuery
|
||||
import suwayomi.tachidesk.graphql.queries.SourceQuery
|
||||
import suwayomi.tachidesk.graphql.queries.TrackQuery
|
||||
import suwayomi.tachidesk.graphql.queries.UpdateQuery
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.GraphQLCursor
|
||||
@@ -76,6 +78,7 @@ val schema =
|
||||
TopLevelObject(MetaQuery()),
|
||||
TopLevelObject(SettingsQuery()),
|
||||
TopLevelObject(SourceQuery()),
|
||||
// TopLevelObject(TrackQuery()),
|
||||
TopLevelObject(UpdateQuery()),
|
||||
),
|
||||
mutations =
|
||||
@@ -91,6 +94,7 @@ val schema =
|
||||
TopLevelObject(MetaMutation()),
|
||||
TopLevelObject(SettingsMutation()),
|
||||
TopLevelObject(SourceMutation()),
|
||||
// TopLevelObject(TrackMutation()),
|
||||
TopLevelObject(UpdateMutation()),
|
||||
),
|
||||
subscriptions =
|
||||
|
||||
@@ -81,6 +81,15 @@ fun <T : Comparable<T>> greaterNotUnique(
|
||||
return greaterNotUniqueImpl(column, idColumn, cursor, String::toLong, toValue)
|
||||
}
|
||||
|
||||
@JvmName("greaterNotUniqueIntKeyIntValue")
|
||||
fun greaterNotUnique(
|
||||
column: Column<EntityID<Int>>,
|
||||
idColumn: Column<EntityID<Int>>,
|
||||
cursor: Cursor,
|
||||
): Op<Boolean> {
|
||||
return greaterNotUniqueImpl(column, idColumn, cursor, String::toInt, String::toInt)
|
||||
}
|
||||
|
||||
private fun <K : Comparable<K>, V : Comparable<V>> greaterNotUniqueImpl(
|
||||
column: Column<V>,
|
||||
idColumn: Column<EntityID<K>>,
|
||||
@@ -93,6 +102,19 @@ private fun <K : Comparable<K>, V : Comparable<V>> greaterNotUniqueImpl(
|
||||
return (column greater value) or ((column eq value) and (idColumn greater id))
|
||||
}
|
||||
|
||||
@JvmName("greaterNotUniqueEntityValue")
|
||||
private fun <K : Comparable<K>, V : Comparable<V>> greaterNotUniqueImpl(
|
||||
column: Column<EntityID<V>>,
|
||||
idColumn: Column<EntityID<K>>,
|
||||
cursor: Cursor,
|
||||
toKey: (String) -> K,
|
||||
toValue: (String) -> V,
|
||||
): Op<Boolean> {
|
||||
val id = toKey(cursor.value.substringBefore('-'))
|
||||
val value = toValue(cursor.value.substringAfter('-'))
|
||||
return (column greater value) or ((column eq value) and (idColumn greater id))
|
||||
}
|
||||
|
||||
@JvmName("greaterNotUniqueStringKey")
|
||||
fun <T : Comparable<T>> greaterNotUnique(
|
||||
column: Column<T>,
|
||||
@@ -125,6 +147,15 @@ fun <T : Comparable<T>> lessNotUnique(
|
||||
return lessNotUniqueImpl(column, idColumn, cursor, String::toLong, toValue)
|
||||
}
|
||||
|
||||
@JvmName("lessNotUniqueIntKeyIntValue")
|
||||
fun lessNotUnique(
|
||||
column: Column<EntityID<Int>>,
|
||||
idColumn: Column<EntityID<Int>>,
|
||||
cursor: Cursor,
|
||||
): Op<Boolean> {
|
||||
return lessNotUniqueImpl(column, idColumn, cursor, String::toInt, String::toInt)
|
||||
}
|
||||
|
||||
private fun <K : Comparable<K>, V : Comparable<V>> lessNotUniqueImpl(
|
||||
column: Column<V>,
|
||||
idColumn: Column<EntityID<K>>,
|
||||
@@ -137,6 +168,19 @@ private fun <K : Comparable<K>, V : Comparable<V>> lessNotUniqueImpl(
|
||||
return (column less value) or ((column eq value) and (idColumn less id))
|
||||
}
|
||||
|
||||
@JvmName("lessNotUniqueEntityValue")
|
||||
private fun <K : Comparable<K>, V : Comparable<V>> lessNotUniqueImpl(
|
||||
column: Column<EntityID<V>>,
|
||||
idColumn: Column<EntityID<K>>,
|
||||
cursor: Cursor,
|
||||
toKey: (String) -> K,
|
||||
toValue: (String) -> V,
|
||||
): Op<Boolean> {
|
||||
val id = toKey(cursor.value.substringBefore('-'))
|
||||
val value = toValue(cursor.value.substringAfter('-'))
|
||||
return (column less value) or ((column eq value) and (idColumn less id))
|
||||
}
|
||||
|
||||
@JvmName("lessNotUniqueStringKey")
|
||||
fun <T : Comparable<T>> lessNotUnique(
|
||||
column: Column<T>,
|
||||
|
||||
@@ -139,6 +139,10 @@ class MangaType(
|
||||
fun source(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<SourceType?> {
|
||||
return dataFetchingEnvironment.getValueFromDataLoader<Long, SourceType?>("SourceDataLoader", sourceId)
|
||||
}
|
||||
|
||||
// fun trackRecords(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<TrackRecordNodeList> {
|
||||
// return dataFetchingEnvironment.getValueFromDataLoader<Int, TrackRecordNodeList>("TrackRecordsForMangaIdDataLoader", id)
|
||||
// }
|
||||
}
|
||||
|
||||
data class MangaNodeList(
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
package suwayomi.tachidesk.graphql.types
|
||||
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
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.impl.track.tracker.Tracker
|
||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class TrackerType(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val icon: String,
|
||||
val isLoggedIn: Boolean,
|
||||
val authUrl: String?,
|
||||
) : Node {
|
||||
constructor(tracker: Tracker) : this(
|
||||
tracker.isLoggedIn,
|
||||
tracker,
|
||||
)
|
||||
|
||||
constructor(isLoggedIn: Boolean, tracker: Tracker) : this(
|
||||
tracker.id,
|
||||
tracker.name,
|
||||
tracker.getLogo(),
|
||||
isLoggedIn,
|
||||
if (isLoggedIn) {
|
||||
null
|
||||
} else {
|
||||
tracker.authUrl()
|
||||
},
|
||||
)
|
||||
|
||||
fun trackRecords(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<TrackRecordNodeList> {
|
||||
return dataFetchingEnvironment.getValueFromDataLoader<Int, TrackRecordNodeList>("TrackRecordsForTrackerIdDataLoader", id)
|
||||
}
|
||||
}
|
||||
|
||||
class TrackRecordType(
|
||||
val id: Int,
|
||||
val mangaId: Int,
|
||||
val syncId: Int,
|
||||
val remoteId: Long,
|
||||
val libraryId: Long?,
|
||||
val title: String,
|
||||
val lastChapterRead: Double,
|
||||
val totalChapters: Int,
|
||||
val status: Int,
|
||||
val score: Double,
|
||||
val remoteUrl: String,
|
||||
val startDate: Long,
|
||||
val finishDate: Long,
|
||||
) : Node {
|
||||
constructor(row: ResultRow) : this(
|
||||
row[TrackRecordTable.id].value,
|
||||
row[TrackRecordTable.mangaId].value,
|
||||
row[TrackRecordTable.syncId],
|
||||
row[TrackRecordTable.remoteId],
|
||||
row[TrackRecordTable.libraryId],
|
||||
row[TrackRecordTable.title],
|
||||
row[TrackRecordTable.lastChapterRead],
|
||||
row[TrackRecordTable.totalChapters],
|
||||
row[TrackRecordTable.status],
|
||||
row[TrackRecordTable.score],
|
||||
row[TrackRecordTable.remoteUrl],
|
||||
row[TrackRecordTable.startDate],
|
||||
row[TrackRecordTable.finishDate],
|
||||
)
|
||||
|
||||
fun displayScore(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<String> {
|
||||
return dataFetchingEnvironment.getValueFromDataLoader<Int, String>("DisplayScoreForTrackRecordDataLoader", id)
|
||||
}
|
||||
|
||||
fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaType> {
|
||||
return dataFetchingEnvironment.getValueFromDataLoader<Int, MangaType>("MangaDataLoader", mangaId)
|
||||
}
|
||||
|
||||
fun tracker(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<TrackerType> {
|
||||
return dataFetchingEnvironment.getValueFromDataLoader<Int, TrackerType>("TrackerDataLoader", syncId)
|
||||
}
|
||||
}
|
||||
|
||||
class TrackSearchType(
|
||||
val syncId: Int,
|
||||
val mediaId: Long,
|
||||
val title: String,
|
||||
val totalChapters: Int,
|
||||
val trackingUrl: String,
|
||||
val coverUrl: String,
|
||||
val summary: String,
|
||||
val publishingStatus: String,
|
||||
val publishingType: String,
|
||||
val startDate: String,
|
||||
) {
|
||||
constructor(trackSearch: TrackSearch) : this(
|
||||
trackSearch.sync_id,
|
||||
trackSearch.media_id,
|
||||
trackSearch.title,
|
||||
trackSearch.total_chapters,
|
||||
trackSearch.tracking_url,
|
||||
trackSearch.cover_url,
|
||||
trackSearch.summary,
|
||||
trackSearch.publishing_status,
|
||||
trackSearch.publishing_type,
|
||||
trackSearch.start_date,
|
||||
)
|
||||
|
||||
fun tracker(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<TrackerType> {
|
||||
return dataFetchingEnvironment.getValueFromDataLoader<Int, TrackerType>("TrackerDataLoader", syncId)
|
||||
}
|
||||
}
|
||||
|
||||
data class TrackRecordNodeList(
|
||||
override val nodes: List<TrackRecordType>,
|
||||
override val edges: List<TrackRecordEdge>,
|
||||
override val pageInfo: PageInfo,
|
||||
override val totalCount: Int,
|
||||
) : NodeList() {
|
||||
data class TrackRecordEdge(
|
||||
override val cursor: Cursor,
|
||||
override val node: TrackRecordType,
|
||||
) : Edge()
|
||||
|
||||
companion object {
|
||||
fun List<TrackRecordType>.toNodeList(): TrackRecordNodeList {
|
||||
return TrackRecordNodeList(
|
||||
nodes = this,
|
||||
edges = getEdges(),
|
||||
pageInfo =
|
||||
PageInfo(
|
||||
hasNextPage = false,
|
||||
hasPreviousPage = false,
|
||||
startCursor = Cursor(0.toString()),
|
||||
endCursor = Cursor(lastIndex.toString()),
|
||||
),
|
||||
totalCount = size,
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<TrackRecordType>.getEdges(): List<TrackRecordEdge> {
|
||||
if (isEmpty()) return emptyList()
|
||||
return listOf(
|
||||
TrackRecordEdge(
|
||||
cursor = Cursor("0"),
|
||||
node = first(),
|
||||
),
|
||||
TrackRecordEdge(
|
||||
cursor = Cursor(lastIndex.toString()),
|
||||
node = last(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class TrackerNodeList(
|
||||
override val nodes: List<TrackerType>,
|
||||
override val edges: List<TrackerEdge>,
|
||||
override val pageInfo: PageInfo,
|
||||
override val totalCount: Int,
|
||||
) : NodeList() {
|
||||
data class TrackerEdge(
|
||||
override val cursor: Cursor,
|
||||
override val node: TrackerType,
|
||||
) : Edge()
|
||||
|
||||
companion object {
|
||||
fun List<TrackerType>.toNodeList(): TrackerNodeList {
|
||||
return TrackerNodeList(
|
||||
nodes = this,
|
||||
edges = getEdges(),
|
||||
pageInfo =
|
||||
PageInfo(
|
||||
hasNextPage = false,
|
||||
hasPreviousPage = false,
|
||||
startCursor = Cursor(0.toString()),
|
||||
endCursor = Cursor(lastIndex.toString()),
|
||||
),
|
||||
totalCount = size,
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<TrackerType>.getEdges(): List<TrackerEdge> {
|
||||
if (isEmpty()) return emptyList()
|
||||
return listOf(
|
||||
TrackerEdge(
|
||||
cursor = Cursor("0"),
|
||||
node = first(),
|
||||
),
|
||||
TrackerEdge(
|
||||
cursor = Cursor(lastIndex.toString()),
|
||||
node = last(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user