feat: implement board detail repository and mutation aggregation
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
package space.hackenslacker.kanbn4droid.app.boarddetail
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
|
||||
import space.hackenslacker.kanbn4droid.app.auth.KanbnApiClient
|
||||
import space.hackenslacker.kanbn4droid.app.auth.SessionStore
|
||||
import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult
|
||||
|
||||
class BoardDetailRepository(
|
||||
private val sessionStore: SessionStore,
|
||||
private val apiKeyStore: ApiKeyStore,
|
||||
private val apiClient: KanbnApiClient,
|
||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) {
|
||||
suspend fun getBoardDetail(boardId: String): BoardsApiResult<BoardDetail> {
|
||||
val normalizedBoardId = boardId.trim()
|
||||
if (normalizedBoardId.isBlank()) {
|
||||
return BoardsApiResult.Failure("Board id is required")
|
||||
}
|
||||
|
||||
val session = when (val sessionResult = session()) {
|
||||
is BoardsApiResult.Success -> sessionResult.value
|
||||
is BoardsApiResult.Failure -> return sessionResult
|
||||
}
|
||||
|
||||
return apiClient.getBoardDetail(
|
||||
baseUrl = session.baseUrl,
|
||||
apiKey = session.apiKey,
|
||||
boardId = normalizedBoardId,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun renameList(listId: String, newTitle: String): BoardsApiResult<Unit> {
|
||||
val normalizedListId = listId.trim()
|
||||
if (normalizedListId.isBlank()) {
|
||||
return BoardsApiResult.Failure("List id is required")
|
||||
}
|
||||
|
||||
val normalizedTitle = newTitle.trim()
|
||||
if (normalizedTitle.isBlank()) {
|
||||
return BoardsApiResult.Failure("List title is required")
|
||||
}
|
||||
|
||||
val session = when (val sessionResult = session()) {
|
||||
is BoardsApiResult.Success -> sessionResult.value
|
||||
is BoardsApiResult.Failure -> return sessionResult
|
||||
}
|
||||
|
||||
return apiClient.renameList(
|
||||
baseUrl = session.baseUrl,
|
||||
apiKey = session.apiKey,
|
||||
listId = normalizedListId,
|
||||
newTitle = normalizedTitle,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun moveCards(cardIds: Collection<String>, targetListId: String): CardBatchMutationResult {
|
||||
val normalizedTargetListId = targetListId.trim()
|
||||
if (normalizedTargetListId.isBlank()) {
|
||||
return CardBatchMutationResult.Failure("Target list id is required")
|
||||
}
|
||||
|
||||
val normalizedCardIds = normalizeCardIds(cardIds)
|
||||
if (normalizedCardIds.isEmpty()) {
|
||||
return CardBatchMutationResult.Failure("At least one card id is required")
|
||||
}
|
||||
|
||||
val session = when (val sessionResult = session()) {
|
||||
is BoardsApiResult.Success -> sessionResult.value
|
||||
is BoardsApiResult.Failure -> return CardBatchMutationResult.Failure(sessionResult.message)
|
||||
}
|
||||
|
||||
val failuresByCardId = linkedMapOf<String, String>()
|
||||
normalizedCardIds.forEach { cardId ->
|
||||
when (
|
||||
val result = apiClient.moveCard(
|
||||
baseUrl = session.baseUrl,
|
||||
apiKey = session.apiKey,
|
||||
cardId = cardId,
|
||||
targetListId = normalizedTargetListId,
|
||||
)
|
||||
) {
|
||||
is BoardsApiResult.Success -> Unit
|
||||
is BoardsApiResult.Failure -> failuresByCardId[cardId] = result.message
|
||||
}
|
||||
}
|
||||
|
||||
return aggregateBatchMutationResult(
|
||||
normalizedCardIds = normalizedCardIds,
|
||||
failuresByCardId = failuresByCardId,
|
||||
partialMessage = "Some cards could not be moved. Please try again.",
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun deleteCards(cardIds: Collection<String>): CardBatchMutationResult {
|
||||
val normalizedCardIds = normalizeCardIds(cardIds)
|
||||
if (normalizedCardIds.isEmpty()) {
|
||||
return CardBatchMutationResult.Failure("At least one card id is required")
|
||||
}
|
||||
|
||||
val session = when (val sessionResult = session()) {
|
||||
is BoardsApiResult.Success -> sessionResult.value
|
||||
is BoardsApiResult.Failure -> return CardBatchMutationResult.Failure(sessionResult.message)
|
||||
}
|
||||
|
||||
val failuresByCardId = linkedMapOf<String, String>()
|
||||
normalizedCardIds.forEach { cardId ->
|
||||
when (val result = apiClient.deleteCard(session.baseUrl, session.apiKey, cardId)) {
|
||||
is BoardsApiResult.Success -> Unit
|
||||
is BoardsApiResult.Failure -> failuresByCardId[cardId] = result.message
|
||||
}
|
||||
}
|
||||
|
||||
return aggregateBatchMutationResult(
|
||||
normalizedCardIds = normalizedCardIds,
|
||||
failuresByCardId = failuresByCardId,
|
||||
partialMessage = "Some cards could not be deleted. Please try again.",
|
||||
)
|
||||
}
|
||||
|
||||
private fun normalizeCardIds(cardIds: Collection<String>): List<String> {
|
||||
return cardIds.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
.distinct()
|
||||
}
|
||||
|
||||
private fun aggregateBatchMutationResult(
|
||||
normalizedCardIds: List<String>,
|
||||
failuresByCardId: Map<String, String>,
|
||||
partialMessage: String,
|
||||
): CardBatchMutationResult {
|
||||
if (failuresByCardId.isEmpty()) {
|
||||
return CardBatchMutationResult.Success
|
||||
}
|
||||
|
||||
if (failuresByCardId.size == normalizedCardIds.size) {
|
||||
val firstFailureMessage = normalizedCardIds
|
||||
.asSequence()
|
||||
.mapNotNull { failuresByCardId[it] }
|
||||
.firstOrNull()
|
||||
?.trim()
|
||||
.orEmpty()
|
||||
.ifBlank { "Unknown error" }
|
||||
return CardBatchMutationResult.Failure(firstFailureMessage)
|
||||
}
|
||||
|
||||
return CardBatchMutationResult.PartialSuccess(
|
||||
failedCardIds = failuresByCardId.keys.toSet(),
|
||||
message = partialMessage,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun session(): BoardsApiResult<SessionSnapshot> {
|
||||
val baseUrl = sessionStore.getBaseUrl()?.takeIf { it.isNotBlank() }
|
||||
?: return BoardsApiResult.Failure("Missing session. Please sign in again.")
|
||||
val apiKey = withContext(ioDispatcher) {
|
||||
apiKeyStore.getApiKey(baseUrl)
|
||||
}.getOrNull()?.takeIf { it.isNotBlank() }
|
||||
?: return BoardsApiResult.Failure("Missing session. Please sign in again.")
|
||||
|
||||
val workspaceId = when (val workspaceResult = resolveWorkspaceId(baseUrl = baseUrl, apiKey = apiKey)) {
|
||||
is BoardsApiResult.Success -> workspaceResult.value
|
||||
is BoardsApiResult.Failure -> return workspaceResult
|
||||
}
|
||||
|
||||
return BoardsApiResult.Success(
|
||||
SessionSnapshot(baseUrl = baseUrl, apiKey = apiKey, workspaceId = workspaceId),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun resolveWorkspaceId(baseUrl: String, apiKey: String): BoardsApiResult<String> {
|
||||
val storedWorkspaceId = sessionStore.getWorkspaceId()?.takeIf { it.isNotBlank() }
|
||||
if (storedWorkspaceId != null) {
|
||||
return BoardsApiResult.Success(storedWorkspaceId)
|
||||
}
|
||||
|
||||
return when (val workspacesResult = apiClient.listWorkspaces(baseUrl, apiKey)) {
|
||||
is BoardsApiResult.Success -> {
|
||||
val first = workspacesResult.value.firstOrNull()?.id
|
||||
?: return BoardsApiResult.Failure("No workspaces available for this account.")
|
||||
sessionStore.saveWorkspaceId(first)
|
||||
BoardsApiResult.Success(first)
|
||||
}
|
||||
|
||||
is BoardsApiResult.Failure -> workspacesResult
|
||||
}
|
||||
}
|
||||
|
||||
private data class SessionSnapshot(
|
||||
val baseUrl: String,
|
||||
val apiKey: String,
|
||||
@Suppress("unused")
|
||||
val workspaceId: String,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user