diff --git a/app-ktor/src/main/kotlin/Main.kt b/app-ktor/src/main/kotlin/Main.kt index 99cc96a9..74f75a97 100644 --- a/app-ktor/src/main/kotlin/Main.kt +++ b/app-ktor/src/main/kotlin/Main.kt @@ -10,7 +10,6 @@ import com.gitlab.sszuev.flashcards.api.cardApiV1 import com.gitlab.sszuev.flashcards.api.dictionaryApiV1 import com.gitlab.sszuev.flashcards.config.ContextConfig import com.gitlab.sszuev.flashcards.config.KeycloakConfig -import com.gitlab.sszuev.flashcards.config.RepositoriesConfig import com.gitlab.sszuev.flashcards.config.RunConfig import com.gitlab.sszuev.flashcards.config.TutorConfig import com.gitlab.sszuev.flashcards.logslib.ExtLogger @@ -81,13 +80,14 @@ fun main(args: Array) = io.ktor.server.jetty.EngineMain.main(args) @KtorExperimentalLocationsAPI @Suppress("unused") fun Application.module( - repositoriesConfig: RepositoriesConfig = RepositoriesConfig(), keycloakConfig: KeycloakConfig = KeycloakConfig(environment.config), runConfig: RunConfig = RunConfig(environment.config), tutorConfig: TutorConfig = TutorConfig(environment.config), ) { logger.info(printGeneralSettings(runConfig, keycloakConfig, tutorConfig)) + val repositories = appRepositories() + val port = environment.config.property("ktor.deployment.port").getString() val keycloakProvider = OAuthServerSettings.OAuth2ServerSettings( @@ -174,12 +174,12 @@ fun Application.module( authenticate("auth-jwt") { this@authenticate.cardApiV1( service = cardService, - repositories = repositoriesConfig.cardRepositories, + repositories = repositories, contextConfig = contextConfig, ) this@authenticate.dictionaryApiV1( service = dictionaryService, - repositories = repositoriesConfig.dictionaryRepositories, + repositories = repositories, contextConfig = contextConfig, ) } @@ -203,12 +203,12 @@ fun Application.module( } else { cardApiV1( service = cardService, - repositories = repositoriesConfig.cardRepositories, + repositories = repositories, contextConfig = contextConfig, ) dictionaryApiV1( service = dictionaryService, - repositories = repositoriesConfig.dictionaryRepositories, + repositories = repositories, contextConfig = contextConfig, ) get("/") { diff --git a/app-ktor/src/main/kotlin/Repositories.kt b/app-ktor/src/main/kotlin/Repositories.kt new file mode 100644 index 00000000..6b9b4cca --- /dev/null +++ b/app-ktor/src/main/kotlin/Repositories.kt @@ -0,0 +1,17 @@ +package com.gitlab.sszuev.flashcards + +import com.gitlab.sszuev.flashcards.dbmem.MemDbCardRepository +import com.gitlab.sszuev.flashcards.dbmem.MemDbDictionaryRepository +import com.gitlab.sszuev.flashcards.dbpg.PgDbCardRepository +import com.gitlab.sszuev.flashcards.dbpg.PgDbDictionaryRepository +import com.gitlab.sszuev.flashcards.speaker.createDirectTTSResourceRepository +import com.gitlab.sszuev.flashcards.speaker.rabbitmq.RMQTTSResourceRepository + +fun appRepositories() = AppRepositories( + prodTTSClientRepository = RMQTTSResourceRepository(), + testTTSClientRepository = createDirectTTSResourceRepository(), + prodCardRepository = PgDbCardRepository(), + testCardRepository = MemDbCardRepository(), + prodDictionaryRepository = PgDbDictionaryRepository(), + testDictionaryRepository = MemDbDictionaryRepository(), +) \ No newline at end of file diff --git a/app-ktor/src/main/kotlin/api/Api.kt b/app-ktor/src/main/kotlin/api/Api.kt index 12b1d223..99ddb8ea 100644 --- a/app-ktor/src/main/kotlin/api/Api.kt +++ b/app-ktor/src/main/kotlin/api/Api.kt @@ -1,7 +1,6 @@ package com.gitlab.sszuev.flashcards.api -import com.gitlab.sszuev.flashcards.CardRepositories -import com.gitlab.sszuev.flashcards.DictionaryRepositories +import com.gitlab.sszuev.flashcards.AppRepositories import com.gitlab.sszuev.flashcards.api.controllers.cards import com.gitlab.sszuev.flashcards.api.controllers.dictionaries import com.gitlab.sszuev.flashcards.api.controllers.sounds @@ -13,7 +12,7 @@ import io.ktor.server.routing.route internal fun Route.cardApiV1( service: CardService, - repositories: CardRepositories, + repositories: AppRepositories, contextConfig: ContextConfig, ) { route("v1/api") { @@ -24,7 +23,7 @@ internal fun Route.cardApiV1( internal fun Route.dictionaryApiV1( service: DictionaryService, - repositories: DictionaryRepositories, + repositories: AppRepositories, contextConfig: ContextConfig, ) { route("v1/api") { diff --git a/app-ktor/src/main/kotlin/api/controllers/CardController.kt b/app-ktor/src/main/kotlin/api/controllers/CardController.kt index 48b1192d..655c6903 100644 --- a/app-ktor/src/main/kotlin/api/controllers/CardController.kt +++ b/app-ktor/src/main/kotlin/api/controllers/CardController.kt @@ -1,7 +1,7 @@ package com.gitlab.sszuev.flashcards.api.controllers +import com.gitlab.sszuev.flashcards.AppRepositories import com.gitlab.sszuev.flashcards.CardContext -import com.gitlab.sszuev.flashcards.CardRepositories import com.gitlab.sszuev.flashcards.api.services.CardService import com.gitlab.sszuev.flashcards.api.v1.models.BaseRequest import com.gitlab.sszuev.flashcards.api.v1.models.CreateCardRequest @@ -26,7 +26,7 @@ private val logger: ExtLogger = logger("com.gitlab.sszuev.flashcards.api.control suspend fun ApplicationCall.getResource( service: CardService, - repositories: CardRepositories, + repositories: AppRepositories, contextConfig: ContextConfig ) { execute(CardOperation.GET_RESOURCE, repositories, logger, contextConfig) { @@ -36,7 +36,7 @@ suspend fun ApplicationCall.getResource( suspend fun ApplicationCall.createCard( service: CardService, - repositories: CardRepositories, + repositories: AppRepositories, contextConfig: ContextConfig ) { execute(CardOperation.CREATE_CARD, repositories, logger, contextConfig) { @@ -46,7 +46,7 @@ suspend fun ApplicationCall.createCard( suspend fun ApplicationCall.updateCard( service: CardService, - repositories: CardRepositories, + repositories: AppRepositories, contextConfig: ContextConfig ) { execute(CardOperation.UPDATE_CARD, repositories, logger, contextConfig) { @@ -56,7 +56,7 @@ suspend fun ApplicationCall.updateCard( suspend fun ApplicationCall.searchCards( service: CardService, - repositories: CardRepositories, + repositories: AppRepositories, contextConfig: ContextConfig ) { execute(CardOperation.SEARCH_CARDS, repositories, logger, contextConfig) { @@ -66,7 +66,7 @@ suspend fun ApplicationCall.searchCards( suspend fun ApplicationCall.getAllCards( service: CardService, - repositories: CardRepositories, + repositories: AppRepositories, contextConfig: ContextConfig ) { execute(CardOperation.GET_ALL_CARDS, repositories, logger, contextConfig) { @@ -76,7 +76,7 @@ suspend fun ApplicationCall.getAllCards( suspend fun ApplicationCall.getCard( service: CardService, - repositories: CardRepositories, + repositories: AppRepositories, contextConfig: ContextConfig ) { execute(CardOperation.GET_CARD, repositories, logger, contextConfig) { @@ -86,7 +86,7 @@ suspend fun ApplicationCall.getCard( suspend fun ApplicationCall.learnCard( service: CardService, - repositories: CardRepositories, + repositories: AppRepositories, contextConfig: ContextConfig ) { execute(CardOperation.LEARN_CARDS, repositories, logger, contextConfig) { @@ -96,7 +96,7 @@ suspend fun ApplicationCall.learnCard( suspend fun ApplicationCall.resetCard( service: CardService, - repositories: CardRepositories, + repositories: AppRepositories, contextConfig: ContextConfig ) { execute(CardOperation.RESET_CARD, repositories, logger, contextConfig) { @@ -106,7 +106,7 @@ suspend fun ApplicationCall.resetCard( suspend fun ApplicationCall.deleteCard( service: CardService, - repositories: CardRepositories, + repositories: AppRepositories, contextConfig: ContextConfig ) { execute(CardOperation.DELETE_CARD, repositories, logger, contextConfig) { @@ -116,7 +116,7 @@ suspend fun ApplicationCall.deleteCard( private suspend inline fun ApplicationCall.execute( operation: CardOperation, - repositories: CardRepositories, + repositories: AppRepositories, logger: ExtLogger, contextConfig: ContextConfig, noinline exec: suspend CardContext.() -> Unit, diff --git a/app-ktor/src/main/kotlin/api/controllers/DictionaryController.kt b/app-ktor/src/main/kotlin/api/controllers/DictionaryController.kt index 52e07db5..5a7d7e6b 100644 --- a/app-ktor/src/main/kotlin/api/controllers/DictionaryController.kt +++ b/app-ktor/src/main/kotlin/api/controllers/DictionaryController.kt @@ -1,7 +1,7 @@ package com.gitlab.sszuev.flashcards.api.controllers +import com.gitlab.sszuev.flashcards.AppRepositories import com.gitlab.sszuev.flashcards.DictionaryContext -import com.gitlab.sszuev.flashcards.DictionaryRepositories import com.gitlab.sszuev.flashcards.api.services.DictionaryService import com.gitlab.sszuev.flashcards.api.v1.models.BaseRequest import com.gitlab.sszuev.flashcards.api.v1.models.CreateDictionaryRequest @@ -22,7 +22,7 @@ private val logger: ExtLogger = logger("com.gitlab.sszuev.flashcards.api.control suspend fun ApplicationCall.getAllDictionaries( service: DictionaryService, - repositories: DictionaryRepositories, + repositories: AppRepositories, contextConfig: ContextConfig ) { execute( @@ -37,7 +37,7 @@ suspend fun ApplicationCall.getAllDictionaries( suspend fun ApplicationCall.createDictionary( service: DictionaryService, - repositories: DictionaryRepositories, + repositories: AppRepositories, contextConfig: ContextConfig ) { execute( @@ -52,7 +52,7 @@ suspend fun ApplicationCall.createDictionary( suspend fun ApplicationCall.deleteDictionary( service: DictionaryService, - repositories: DictionaryRepositories, + repositories: AppRepositories, contextConfig: ContextConfig ) { execute( @@ -67,7 +67,7 @@ suspend fun ApplicationCall.deleteDictionary( suspend fun ApplicationCall.downloadDictionary( service: DictionaryService, - repositories: DictionaryRepositories, + repositories: AppRepositories, contextConfig: ContextConfig ) { execute( @@ -82,7 +82,7 @@ suspend fun ApplicationCall.downloadDictionary( suspend fun ApplicationCall.uploadDictionary( service: DictionaryService, - repositories: DictionaryRepositories, + repositories: AppRepositories, contextConfig: ContextConfig ) { execute( @@ -97,7 +97,7 @@ suspend fun ApplicationCall.uploadDictionary( private suspend inline fun ApplicationCall.execute( operation: DictionaryOperation, - repositories: DictionaryRepositories, + repositories: AppRepositories, logger: ExtLogger, contextConfig: ContextConfig, noinline exec: suspend DictionaryContext.() -> Unit, diff --git a/app-ktor/src/main/kotlin/api/controllers/Rest.kt b/app-ktor/src/main/kotlin/api/controllers/Rest.kt index 2f0aedc0..e8bbc174 100644 --- a/app-ktor/src/main/kotlin/api/controllers/Rest.kt +++ b/app-ktor/src/main/kotlin/api/controllers/Rest.kt @@ -1,7 +1,6 @@ package com.gitlab.sszuev.flashcards.api.controllers -import com.gitlab.sszuev.flashcards.CardRepositories -import com.gitlab.sszuev.flashcards.DictionaryRepositories +import com.gitlab.sszuev.flashcards.AppRepositories import com.gitlab.sszuev.flashcards.api.services.CardService import com.gitlab.sszuev.flashcards.api.services.DictionaryService import com.gitlab.sszuev.flashcards.config.ContextConfig @@ -12,7 +11,7 @@ import io.ktor.server.routing.route fun Route.cards( service: CardService, - repositories: CardRepositories, + repositories: AppRepositories, contextConfig: ContextConfig, ) { route("cards") { @@ -45,7 +44,7 @@ fun Route.cards( fun Route.sounds( service: CardService, - repositories: CardRepositories, + repositories: AppRepositories, contextConfig: ContextConfig, ) { route("sounds") { @@ -57,7 +56,7 @@ fun Route.sounds( fun Route.dictionaries( service: DictionaryService, - repositories: DictionaryRepositories, + repositories: AppRepositories, contextConfig: ContextConfig, ) { route("dictionaries") { diff --git a/app-ktor/src/main/kotlin/config/RepositoriesConfig.kt b/app-ktor/src/main/kotlin/config/RepositoriesConfig.kt deleted file mode 100644 index 86df23fa..00000000 --- a/app-ktor/src/main/kotlin/config/RepositoriesConfig.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.gitlab.sszuev.flashcards.config - -import com.gitlab.sszuev.flashcards.CardRepositories -import com.gitlab.sszuev.flashcards.DictionaryRepositories -import com.gitlab.sszuev.flashcards.dbmem.MemDbCardRepository -import com.gitlab.sszuev.flashcards.dbmem.MemDbDictionaryRepository -import com.gitlab.sszuev.flashcards.dbmem.MemDbUserRepository -import com.gitlab.sszuev.flashcards.dbpg.PgDbCardRepository -import com.gitlab.sszuev.flashcards.dbpg.PgDbDictionaryRepository -import com.gitlab.sszuev.flashcards.dbpg.PgDbUserRepository -import com.gitlab.sszuev.flashcards.repositories.DbCardRepository -import com.gitlab.sszuev.flashcards.repositories.DbDictionaryRepository -import com.gitlab.sszuev.flashcards.repositories.DbUserRepository -import com.gitlab.sszuev.flashcards.repositories.TTSResourceRepository -import com.gitlab.sszuev.flashcards.speaker.createDirectTTSResourceRepository -import com.gitlab.sszuev.flashcards.speaker.rabbitmq.RMQTTSResourceRepository - -data class RepositoriesConfig( - val prodTTSClientRepository: TTSResourceRepository = RMQTTSResourceRepository(), - val testTTSClientRepository: TTSResourceRepository = createDirectTTSResourceRepository(), - val prodCardRepository: DbCardRepository = PgDbCardRepository(), - val testCardRepository: DbCardRepository = MemDbCardRepository(), - val prodDictionaryRepository: DbDictionaryRepository = PgDbDictionaryRepository(), - val testDictionaryRepository: DbDictionaryRepository = MemDbDictionaryRepository(), - val prodUserRepository: DbUserRepository = PgDbUserRepository(), - val testUserRepository: DbUserRepository = MemDbUserRepository(), -) { - - val cardRepositories by lazy { - CardRepositories( - prodTTSClientRepository = this.prodTTSClientRepository, - testTTSClientRepository = this.testTTSClientRepository, - prodCardRepository = this.prodCardRepository, - testCardRepository = this.testCardRepository, - prodUserRepository = this.prodUserRepository, - testUserRepository = this.testUserRepository, - ) - } - - val dictionaryRepositories by lazy { - DictionaryRepositories( - prodDictionaryRepository = this.prodDictionaryRepository, - testDictionaryRepository = this.testDictionaryRepository, - prodUserRepository = this.prodUserRepository, - testUserRepository = this.testUserRepository, - prodCardRepository = this.prodCardRepository, - testCardRepository = this.testCardRepository, - ) - } -} \ No newline at end of file diff --git a/app-ktor/src/main/resources/data/dictionaries.csv b/app-ktor/src/main/resources/data/dictionaries.csv index 3b73692b..466e7a37 100644 --- a/app-ktor/src/main/resources/data/dictionaries.csv +++ b/app-ktor/src/main/resources/data/dictionaries.csv @@ -1,3 +1,3 @@ id,name,user_id,source_lang,target_lang,details,changed_at -1,Irregular Verbs,42,en,ru,{},2022-12-26T16:04:14 -2,Weather,42,en,ru,{},2022-12-26T16:04:14 \ No newline at end of file +1,Irregular Verbs,c9a414f5-3f75-4494-b664-f4c8b33ff4e6,en,ru,{},2022-12-26T16:04:14 +2,Weather,c9a414f5-3f75-4494-b664-f4c8b33ff4e6,en,ru,{},2022-12-26T16:04:14 \ No newline at end of file diff --git a/app-ktor/src/main/resources/data/users.csv b/app-ktor/src/main/resources/data/users.csv deleted file mode 100644 index 21c35afe..00000000 --- a/app-ktor/src/main/resources/data/users.csv +++ /dev/null @@ -1,2 +0,0 @@ -id,uuid,details,changed_at -42,c9a414f5-3f75-4494-b664-f4c8b33ff4e6,{},2022-12-26T16:04:14 \ No newline at end of file diff --git a/app-ktor/src/test/kotlin/api/controllers/CardControllerMockkTest.kt b/app-ktor/src/test/kotlin/api/controllers/CardControllerMockkTest.kt index 06fa8406..b8ab69c4 100644 --- a/app-ktor/src/test/kotlin/api/controllers/CardControllerMockkTest.kt +++ b/app-ktor/src/test/kotlin/api/controllers/CardControllerMockkTest.kt @@ -1,7 +1,7 @@ package com.gitlab.sszuev.flashcards.api.controllers +import com.gitlab.sszuev.flashcards.AppRepositories import com.gitlab.sszuev.flashcards.CardContext -import com.gitlab.sszuev.flashcards.CardRepositories import com.gitlab.sszuev.flashcards.api.services.CardService import com.gitlab.sszuev.flashcards.api.v1.models.BaseRequest import com.gitlab.sszuev.flashcards.api.v1.models.BaseResponse @@ -112,7 +112,7 @@ internal class CardControllerMockkTest { coEvery { service.serviceMethod(any()) } throws TestException(msg) - val repositories = mockk() + val repositories = mockk() val tutorConfig = mockk(relaxed = true) val runConfig = mockk(relaxed = true) diff --git a/app-ktor/src/test/kotlin/api/controllers/CardControllerRunTest.kt b/app-ktor/src/test/kotlin/api/controllers/CardControllerRunTest.kt index af8dc4ce..1d9a2227 100644 --- a/app-ktor/src/test/kotlin/api/controllers/CardControllerRunTest.kt +++ b/app-ktor/src/test/kotlin/api/controllers/CardControllerRunTest.kt @@ -117,7 +117,8 @@ internal class CardControllerRunTest { "spell of cold weather" ), res.card!!.words!!.single().examples?.map { it.example } ) - Assertions.assertNull(res.card!!.words!!.single().sound) + Assertions.assertEquals("en:weather", res.card!!.words!!.single().sound) + Assertions.assertEquals("en:weather", res.card!!.sound) Assertions.assertNull(res.card!!.answered) Assertions.assertEquals(emptyMap(), res.card!!.details) } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 868ce87a..2a4181c1 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,5 +1,3 @@ -@file:Suppress("UNUSED_VARIABLE") - plugins { kotlin("multiplatform") } @@ -14,6 +12,7 @@ kotlin { val kotlinDatetimeVersion: String by project val commonMain by getting { dependencies { + implementation(project(":db-common")) implementation(kotlin("stdlib-common")) api("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinDatetimeVersion") } diff --git a/common/src/commonMain/kotlin/DictionaryRepositories.kt b/common/src/commonMain/kotlin/AppRepositories.kt similarity index 64% rename from common/src/commonMain/kotlin/DictionaryRepositories.kt rename to common/src/commonMain/kotlin/AppRepositories.kt index b46704b8..401492fb 100644 --- a/common/src/commonMain/kotlin/DictionaryRepositories.kt +++ b/common/src/commonMain/kotlin/AppRepositories.kt @@ -1,32 +1,24 @@ package com.gitlab.sszuev.flashcards import com.gitlab.sszuev.flashcards.model.common.AppMode -import com.gitlab.sszuev.flashcards.model.common.AppRepositories import com.gitlab.sszuev.flashcards.repositories.DbCardRepository import com.gitlab.sszuev.flashcards.repositories.DbDictionaryRepository -import com.gitlab.sszuev.flashcards.repositories.DbUserRepository import com.gitlab.sszuev.flashcards.repositories.NoOpDbCardRepository import com.gitlab.sszuev.flashcards.repositories.NoOpDbDictionaryRepository -import com.gitlab.sszuev.flashcards.repositories.NoOpDbUserRepository +import com.gitlab.sszuev.flashcards.repositories.NoOpTTSResourceRepository +import com.gitlab.sszuev.flashcards.repositories.TTSResourceRepository -data class DictionaryRepositories( +data class AppRepositories( + private val prodTTSClientRepository: TTSResourceRepository = NoOpTTSResourceRepository, + private val testTTSClientRepository: TTSResourceRepository = NoOpTTSResourceRepository, private val prodDictionaryRepository: DbDictionaryRepository = NoOpDbDictionaryRepository, private val testDictionaryRepository: DbDictionaryRepository = NoOpDbDictionaryRepository, - private val prodUserRepository: DbUserRepository = NoOpDbUserRepository, - private val testUserRepository: DbUserRepository = NoOpDbUserRepository, private val prodCardRepository: DbCardRepository = NoOpDbCardRepository, private val testCardRepository: DbCardRepository = NoOpDbCardRepository, -): AppRepositories { - companion object { - val NO_OP_REPOSITORIES = DictionaryRepositories() - } +) { - override fun userRepository(mode: AppMode): DbUserRepository { - return when(mode) { - AppMode.PROD -> prodUserRepository - AppMode.TEST -> testUserRepository - AppMode.STUB -> NoOpDbUserRepository - } + companion object { + val NO_OP_REPOSITORIES = AppRepositories() } fun dictionaryRepository(mode: AppMode): DbDictionaryRepository { @@ -44,4 +36,12 @@ data class DictionaryRepositories( AppMode.STUB -> NoOpDbCardRepository } } + + fun ttsClientRepository(mode: AppMode): TTSResourceRepository { + return when (mode) { + AppMode.PROD -> prodTTSClientRepository + AppMode.TEST -> testTTSClientRepository + AppMode.STUB -> NoOpTTSResourceRepository + } + } } \ No newline at end of file diff --git a/common/src/commonMain/kotlin/CardContext.kt b/common/src/commonMain/kotlin/CardContext.kt index 68c0b1df..10e25e9d 100644 --- a/common/src/commonMain/kotlin/CardContext.kt +++ b/common/src/commonMain/kotlin/CardContext.kt @@ -7,7 +7,6 @@ import com.gitlab.sszuev.flashcards.model.common.AppMode import com.gitlab.sszuev.flashcards.model.common.AppRequestId import com.gitlab.sszuev.flashcards.model.common.AppStatus import com.gitlab.sszuev.flashcards.model.common.AppStub -import com.gitlab.sszuev.flashcards.model.common.AppUserEntity import com.gitlab.sszuev.flashcards.model.common.NONE import com.gitlab.sszuev.flashcards.model.domain.CardEntity import com.gitlab.sszuev.flashcards.model.domain.CardFilter @@ -25,7 +24,7 @@ import kotlinx.datetime.Instant data class CardContext( override val operation: CardOperation = CardOperation.NONE, override val timestamp: Instant = Instant.NONE, - override val repositories: CardRepositories = CardRepositories.NO_OP_REPOSITORIES, + override val repositories: AppRepositories = AppRepositories.NO_OP_REPOSITORIES, override val errors: MutableList = mutableListOf(), override val config: AppConfig = AppConfig.DEFAULT, @@ -35,7 +34,6 @@ data class CardContext( override var requestId: AppRequestId = AppRequestId.NONE, override var requestAppAuthId: AppAuthId = AppAuthId.NONE, override var normalizedRequestAppAuthId: AppAuthId = AppAuthId.NONE, - override var contextUserEntity: AppUserEntity = AppUserEntity.EMPTY, // get word resource by id (for TTS) var requestTTSResourceGet: TTSResourceGet = TTSResourceGet.NONE, diff --git a/common/src/commonMain/kotlin/CardRepositories.kt b/common/src/commonMain/kotlin/CardRepositories.kt deleted file mode 100644 index a295bdfc..00000000 --- a/common/src/commonMain/kotlin/CardRepositories.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.gitlab.sszuev.flashcards - -import com.gitlab.sszuev.flashcards.model.common.AppMode -import com.gitlab.sszuev.flashcards.model.common.AppRepositories -import com.gitlab.sszuev.flashcards.repositories.* - -data class CardRepositories( - private val prodTTSClientRepository: TTSResourceRepository = NoOpTTSResourceRepository, - private val testTTSClientRepository: TTSResourceRepository = NoOpTTSResourceRepository, - private val prodCardRepository: DbCardRepository = NoOpDbCardRepository, - private val testCardRepository: DbCardRepository = NoOpDbCardRepository, - private val prodUserRepository: DbUserRepository = NoOpDbUserRepository, - private val testUserRepository: DbUserRepository = NoOpDbUserRepository, -): AppRepositories { - companion object { - val NO_OP_REPOSITORIES = CardRepositories() - } - - override fun userRepository(mode: AppMode): DbUserRepository { - return when(mode) { - AppMode.PROD -> prodUserRepository - AppMode.TEST -> testUserRepository - AppMode.STUB -> NoOpDbUserRepository - } - } - - fun cardRepository(mode: AppMode): DbCardRepository { - return when (mode) { - AppMode.PROD -> prodCardRepository - AppMode.TEST -> testCardRepository - AppMode.STUB -> NoOpDbCardRepository - } - } - - fun ttsClientRepository(mode: AppMode): TTSResourceRepository { - return when(mode) { - AppMode.PROD -> prodTTSClientRepository - AppMode.TEST -> testTTSClientRepository - AppMode.STUB -> NoOpTTSResourceRepository - } - } -} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/DictionaryContext.kt b/common/src/commonMain/kotlin/DictionaryContext.kt index 400a1f30..e4d78a27 100644 --- a/common/src/commonMain/kotlin/DictionaryContext.kt +++ b/common/src/commonMain/kotlin/DictionaryContext.kt @@ -7,7 +7,6 @@ import com.gitlab.sszuev.flashcards.model.common.AppMode import com.gitlab.sszuev.flashcards.model.common.AppRequestId import com.gitlab.sszuev.flashcards.model.common.AppStatus import com.gitlab.sszuev.flashcards.model.common.AppStub -import com.gitlab.sszuev.flashcards.model.common.AppUserEntity import com.gitlab.sszuev.flashcards.model.common.NONE import com.gitlab.sszuev.flashcards.model.domain.DictionaryEntity import com.gitlab.sszuev.flashcards.model.domain.DictionaryId @@ -16,9 +15,9 @@ import com.gitlab.sszuev.flashcards.model.domain.ResourceEntity import kotlinx.datetime.Instant data class DictionaryContext( - override val repositories: DictionaryRepositories = DictionaryRepositories.NO_OP_REPOSITORIES, override val operation: DictionaryOperation = DictionaryOperation.NONE, override val timestamp: Instant = Instant.NONE, + override val repositories: AppRepositories = AppRepositories.NO_OP_REPOSITORIES, override val errors: MutableList = mutableListOf(), override val config: AppConfig = AppConfig.DEFAULT, @@ -28,7 +27,6 @@ data class DictionaryContext( override var requestId: AppRequestId = AppRequestId.NONE, override var requestAppAuthId: AppAuthId = AppAuthId.NONE, override var normalizedRequestAppAuthId: AppAuthId = AppAuthId.NONE, - override var contextUserEntity: AppUserEntity = AppUserEntity.EMPTY, // get all dictionaries' list response: var responseDictionaryEntityList: List = listOf(), diff --git a/common/src/commonMain/kotlin/model/common/AppContext.kt b/common/src/commonMain/kotlin/model/common/AppContext.kt index f1316f26..1a0602c4 100644 --- a/common/src/commonMain/kotlin/model/common/AppContext.kt +++ b/common/src/commonMain/kotlin/model/common/AppContext.kt @@ -1,6 +1,7 @@ package com.gitlab.sszuev.flashcards.model.common import com.gitlab.sszuev.flashcards.AppConfig +import com.gitlab.sszuev.flashcards.AppRepositories import kotlinx.datetime.Instant interface AppContext { @@ -18,7 +19,6 @@ interface AppContext { // get user: var requestAppAuthId: AppAuthId var normalizedRequestAppAuthId: AppAuthId - var contextUserEntity: AppUserEntity } private val none = Instant.fromEpochMilliseconds(Long.MIN_VALUE) diff --git a/common/src/commonMain/kotlin/model/common/AppRepositories.kt b/common/src/commonMain/kotlin/model/common/AppRepositories.kt deleted file mode 100644 index 1eb9a56e..00000000 --- a/common/src/commonMain/kotlin/model/common/AppRepositories.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.gitlab.sszuev.flashcards.model.common - -interface AppRepositories { - fun userRepository(mode: AppMode): AppUserRepository -} - -interface AppUserRepository { - fun getUser(authId: AppAuthId): AppUserResponse -} - -interface AppUserResponse { - val user: AppUserEntity - val errors: List -} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/model/common/AppUserEntity.kt b/common/src/commonMain/kotlin/model/common/AppUserEntity.kt deleted file mode 100644 index eac3f419..00000000 --- a/common/src/commonMain/kotlin/model/common/AppUserEntity.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.gitlab.sszuev.flashcards.model.common - -data class AppUserEntity(val id: AppUserId, val authId: AppAuthId) { - companion object { - val EMPTY = AppUserEntity(id = AppUserId.NONE, authId = AppAuthId.NONE) - } -} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/model/common/AppUserId.kt b/common/src/commonMain/kotlin/model/common/AppUserId.kt deleted file mode 100644 index 0203c42a..00000000 --- a/common/src/commonMain/kotlin/model/common/AppUserId.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.gitlab.sszuev.flashcards.model.common - -import com.gitlab.sszuev.flashcards.model.Id - -@JvmInline -value class AppUserId(private val id: String) : Id { - override fun asString() = id - - companion object { - val NONE = AppUserId("") - } -} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/model/domain/CardFilter.kt b/common/src/commonMain/kotlin/model/domain/CardFilter.kt index 78a3ebee..d50f03a6 100644 --- a/common/src/commonMain/kotlin/model/domain/CardFilter.kt +++ b/common/src/commonMain/kotlin/model/domain/CardFilter.kt @@ -4,7 +4,10 @@ data class CardFilter( val dictionaryIds: List = emptyList(), val random: Boolean = false, val length: Int = 0, - val withUnknown: Boolean = false, + /** + * `true` to return only unknown words + */ + val onlyUnknown: Boolean = false, ) { companion object { val EMPTY = CardFilter() diff --git a/common/src/commonMain/kotlin/model/domain/DictionaryEntity.kt b/common/src/commonMain/kotlin/model/domain/DictionaryEntity.kt index 568a6c03..5164ae43 100644 --- a/common/src/commonMain/kotlin/model/domain/DictionaryEntity.kt +++ b/common/src/commonMain/kotlin/model/domain/DictionaryEntity.kt @@ -1,7 +1,10 @@ package com.gitlab.sszuev.flashcards.model.domain +import com.gitlab.sszuev.flashcards.model.common.AppAuthId + data class DictionaryEntity( val dictionaryId: DictionaryId = DictionaryId.NONE, + val userId: AppAuthId = AppAuthId.NONE, val name: String = "", val sourceLang: LangEntity = LangEntity.EMPTY, val targetLang: LangEntity = LangEntity.EMPTY, diff --git a/common/src/commonMain/kotlin/repositories/DbCardRepository.kt b/common/src/commonMain/kotlin/repositories/DbCardRepository.kt deleted file mode 100644 index 552ec856..00000000 --- a/common/src/commonMain/kotlin/repositories/DbCardRepository.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.gitlab.sszuev.flashcards.repositories - -import com.gitlab.sszuev.flashcards.model.common.AppError -import com.gitlab.sszuev.flashcards.model.common.AppUserId -import com.gitlab.sszuev.flashcards.model.domain.CardEntity -import com.gitlab.sszuev.flashcards.model.domain.CardFilter -import com.gitlab.sszuev.flashcards.model.domain.CardId -import com.gitlab.sszuev.flashcards.model.domain.DictionaryEntity -import com.gitlab.sszuev.flashcards.model.domain.DictionaryId - -/** - * Database repository to work with cards. - */ -interface DbCardRepository { - - /** - * Gets card by id. - */ - fun getCard(userId: AppUserId, cardId: CardId): CardDbResponse - - /** - * Gets all cards by dictionaryId. - */ - fun getAllCards(userId: AppUserId, dictionaryId: DictionaryId): CardsDbResponse - - /** - * Searches cards by filter. - */ - fun searchCard(userId: AppUserId, filter: CardFilter): CardsDbResponse - - /** - * Creates card. - */ - fun createCard(userId: AppUserId, cardEntity: CardEntity): CardDbResponse - - /** - * Updates. - */ - fun updateCard(userId: AppUserId, cardEntity: CardEntity): CardDbResponse - - /** - * Performs bulk update. - */ - fun updateCards(userId: AppUserId, cardIds: Iterable, update: (CardEntity) -> CardEntity): CardsDbResponse - - /** - * Resets status. - */ - fun resetCard(userId: AppUserId, cardId: CardId): CardDbResponse - - /** - * Deletes card by id. - */ - fun removeCard(userId: AppUserId, cardId: CardId): RemoveCardDbResponse -} - -data class CardsDbResponse( - val cards: List = emptyList(), - val dictionaries: List = emptyList(), - val errors: List = emptyList(), -) { - - companion object { - val EMPTY = CardsDbResponse(cards = emptyList(), dictionaries = emptyList(), errors = emptyList()) - } -} - -data class CardDbResponse( - val card: CardEntity = CardEntity.EMPTY, - val errors: List = emptyList(), -) { - constructor(error: AppError) : this(errors = listOf(error)) - - companion object { - val EMPTY = CardDbResponse(card = CardEntity.EMPTY) - } -} - -data class RemoveCardDbResponse( - val card: CardEntity = CardEntity.EMPTY, - val errors: List = emptyList(), -) { - constructor(error: AppError) : this(errors = listOf(error)) - - companion object { - val EMPTY = RemoveCardDbResponse(errors = emptyList()) - } -} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/repositories/DbDictionaryRepository.kt b/common/src/commonMain/kotlin/repositories/DbDictionaryRepository.kt deleted file mode 100644 index ab0b3e29..00000000 --- a/common/src/commonMain/kotlin/repositories/DbDictionaryRepository.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.gitlab.sszuev.flashcards.repositories - -import com.gitlab.sszuev.flashcards.model.common.AppError -import com.gitlab.sszuev.flashcards.model.common.AppUserId -import com.gitlab.sszuev.flashcards.model.domain.DictionaryEntity -import com.gitlab.sszuev.flashcards.model.domain.DictionaryId -import com.gitlab.sszuev.flashcards.model.domain.ResourceEntity - -interface DbDictionaryRepository { - fun getAllDictionaries(userId: AppUserId): DictionariesDbResponse - - fun createDictionary(userId: AppUserId, entity: DictionaryEntity): DictionaryDbResponse - - fun removeDictionary(userId: AppUserId, dictionaryId: DictionaryId): RemoveDictionaryDbResponse - - fun importDictionary(userId: AppUserId, dictionaryId: DictionaryId): ImportDictionaryDbResponse - - fun exportDictionary(userId: AppUserId, resource: ResourceEntity): DictionaryDbResponse -} - -data class DictionariesDbResponse( - val dictionaries: List, - val errors: List = emptyList() -) { - companion object { - val EMPTY = DictionariesDbResponse(dictionaries = emptyList(), errors = emptyList()) - } -} - -data class RemoveDictionaryDbResponse( - val dictionary: DictionaryEntity = DictionaryEntity.EMPTY, - val errors: List = emptyList(), -) { - constructor(error: AppError) : this(errors = listOf(error)) - - companion object { - val EMPTY = RemoveDictionaryDbResponse() - } -} - -data class ImportDictionaryDbResponse( - val resource: ResourceEntity = ResourceEntity.DUMMY, - val errors: List = emptyList() -) { - constructor(error: AppError) : this(errors = listOf(error)) - - companion object { - val EMPTY = ImportDictionaryDbResponse() - } -} - -data class DictionaryDbResponse( - val dictionary: DictionaryEntity = DictionaryEntity.EMPTY, - val errors: List = emptyList() -) { - constructor(error: AppError) : this(errors = listOf(error)) - - companion object { - val EMPTY = DictionaryDbResponse() - } -} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/repositories/DbUserRepository.kt b/common/src/commonMain/kotlin/repositories/DbUserRepository.kt deleted file mode 100644 index 9c778e6c..00000000 --- a/common/src/commonMain/kotlin/repositories/DbUserRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.gitlab.sszuev.flashcards.repositories - -import com.gitlab.sszuev.flashcards.model.common.* - -interface DbUserRepository : AppUserRepository { - override fun getUser(authId: AppAuthId): UserEntityDbResponse -} - -data class UserEntityDbResponse( - override val user: AppUserEntity, - override val errors: List = emptyList() -) : AppUserResponse { - companion object { - val EMPTY = UserEntityDbResponse(user = AppUserEntity.EMPTY) - } -} diff --git a/common/src/commonMain/kotlin/repositories/NoOpDbCardRepository.kt b/common/src/commonMain/kotlin/repositories/NoOpDbCardRepository.kt deleted file mode 100644 index 3645c3ef..00000000 --- a/common/src/commonMain/kotlin/repositories/NoOpDbCardRepository.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.gitlab.sszuev.flashcards.repositories - -import com.gitlab.sszuev.flashcards.model.common.AppUserId -import com.gitlab.sszuev.flashcards.model.domain.CardEntity -import com.gitlab.sszuev.flashcards.model.domain.CardFilter -import com.gitlab.sszuev.flashcards.model.domain.CardId -import com.gitlab.sszuev.flashcards.model.domain.DictionaryId - -object NoOpDbCardRepository : DbCardRepository { - override fun getCard(userId: AppUserId, cardId: CardId): CardDbResponse { - noOp() - } - - override fun getAllCards(userId: AppUserId, dictionaryId: DictionaryId): CardsDbResponse { - noOp() - } - - override fun searchCard(userId: AppUserId, filter: CardFilter): CardsDbResponse { - noOp() - } - - override fun createCard(userId: AppUserId, cardEntity: CardEntity): CardDbResponse { - noOp() - } - - override fun updateCard(userId: AppUserId, cardEntity: CardEntity): CardDbResponse { - noOp() - } - - override fun updateCards( - userId: AppUserId, - cardIds: Iterable, - update: (CardEntity) -> CardEntity - ): CardsDbResponse { - noOp() - } - - override fun resetCard(userId: AppUserId, cardId: CardId): CardDbResponse { - noOp() - } - - override fun removeCard(userId: AppUserId, cardId: CardId): RemoveCardDbResponse { - noOp() - } - - private fun noOp(): Nothing { - error("Must not be called.") - } -} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/repositories/NoOpDbDictionaryRepository.kt b/common/src/commonMain/kotlin/repositories/NoOpDbDictionaryRepository.kt deleted file mode 100644 index 28f0927a..00000000 --- a/common/src/commonMain/kotlin/repositories/NoOpDbDictionaryRepository.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.gitlab.sszuev.flashcards.repositories - -import com.gitlab.sszuev.flashcards.model.common.AppUserId -import com.gitlab.sszuev.flashcards.model.domain.DictionaryEntity -import com.gitlab.sszuev.flashcards.model.domain.DictionaryId -import com.gitlab.sszuev.flashcards.model.domain.ResourceEntity - -object NoOpDbDictionaryRepository : DbDictionaryRepository { - override fun getAllDictionaries(userId: AppUserId): DictionariesDbResponse { - noOp() - } - - override fun createDictionary(userId: AppUserId, entity: DictionaryEntity): DictionaryDbResponse { - noOp() - } - - override fun removeDictionary(userId: AppUserId, dictionaryId: DictionaryId): RemoveDictionaryDbResponse { - noOp() - } - - override fun importDictionary(userId: AppUserId, dictionaryId: DictionaryId): ImportDictionaryDbResponse { - noOp() - } - - override fun exportDictionary(userId: AppUserId, resource: ResourceEntity): DictionaryDbResponse { - noOp() - } - - private fun noOp(): Nothing { - error("Must not be called.") - } -} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/repositories/NoOpDbUserRepository.kt b/common/src/commonMain/kotlin/repositories/NoOpDbUserRepository.kt deleted file mode 100644 index 05957934..00000000 --- a/common/src/commonMain/kotlin/repositories/NoOpDbUserRepository.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.gitlab.sszuev.flashcards.repositories - -import com.gitlab.sszuev.flashcards.model.common.AppAuthId - -object NoOpDbUserRepository: DbUserRepository { - override fun getUser(authId: AppAuthId): UserEntityDbResponse { - noOp() - } - - private fun noOp(): Nothing { - error("Must not be called.") - } -} \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 7a499be3..54bf8901 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { val kotlinCoroutinesVersion: String by project implementation(project(":cor-lib")) + implementation(project(":db-common")) implementation(project(":common")) implementation(project(":specs")) diff --git a/core/src/main/kotlin/CardCorProcessor.kt b/core/src/main/kotlin/CardCorProcessor.kt index cbc8b10e..fba6fe4c 100644 --- a/core/src/main/kotlin/CardCorProcessor.kt +++ b/core/src/main/kotlin/CardCorProcessor.kt @@ -5,11 +5,10 @@ import com.gitlab.sszuev.flashcards.core.normalizers.normalizers import com.gitlab.sszuev.flashcards.core.processes.processCardSearch import com.gitlab.sszuev.flashcards.core.processes.processCreateCard import com.gitlab.sszuev.flashcards.core.processes.processDeleteCard -import com.gitlab.sszuev.flashcards.core.processes.processFindUser import com.gitlab.sszuev.flashcards.core.processes.processGetAllCards import com.gitlab.sszuev.flashcards.core.processes.processGetCard import com.gitlab.sszuev.flashcards.core.processes.processLearnCards -import com.gitlab.sszuev.flashcards.core.processes.processResetCards +import com.gitlab.sszuev.flashcards.core.processes.processResetCard import com.gitlab.sszuev.flashcards.core.processes.processResource import com.gitlab.sszuev.flashcards.core.processes.processUpdateCard import com.gitlab.sszuev.flashcards.core.stubs.cardStubSuccess @@ -65,7 +64,6 @@ class CardCorProcessor { validateResourceGetWord() } runs(CardOperation.GET_RESOURCE) { - processFindUser(CardOperation.GET_RESOURCE) processResource() } } @@ -86,7 +84,6 @@ class CardCorProcessor { validateCardFilterDictionaryIds { it.normalizedRequestCardFilter } } runs(CardOperation.SEARCH_CARDS) { - processFindUser(CardOperation.SEARCH_CARDS) processCardSearch() } } @@ -105,7 +102,6 @@ class CardCorProcessor { validateDictionaryId { (it as CardContext).normalizedRequestDictionaryId } } runs(CardOperation.GET_ALL_CARDS) { - processFindUser(CardOperation.GET_ALL_CARDS) processGetAllCards() } } @@ -133,7 +129,6 @@ class CardCorProcessor { validateCardEntityWords { it.normalizedRequestCardEntity } } runs(CardOperation.CREATE_CARD) { - processFindUser(CardOperation.CREATE_CARD) processCreateCard() } } @@ -161,7 +156,6 @@ class CardCorProcessor { validateCardEntityWords { it.normalizedRequestCardEntity } } runs(CardOperation.UPDATE_CARD) { - processFindUser(CardOperation.UPDATE_CARD) processUpdateCard() } } @@ -182,7 +176,6 @@ class CardCorProcessor { validateCardLearnListDetails { it.normalizedRequestCardLearnList } } runs(CardOperation.LEARN_CARDS) { - processFindUser(CardOperation.LEARN_CARDS) processLearnCards() } } @@ -201,7 +194,6 @@ class CardCorProcessor { validateCardId { it.normalizedRequestCardEntityId } } runs(CardOperation.GET_CARD) { - processFindUser(CardOperation.GET_CARD) processGetCard() } } @@ -218,8 +210,7 @@ class CardCorProcessor { validateCardId { it.normalizedRequestCardEntityId } } runs(CardOperation.RESET_CARD) { - processFindUser(CardOperation.RESET_CARD) - processResetCards() + processResetCard() } } @@ -235,7 +226,6 @@ class CardCorProcessor { validateCardId { it.normalizedRequestCardEntityId } } runs(CardOperation.DELETE_CARD) { - processFindUser(CardOperation.DELETE_CARD) processDeleteCard() } } diff --git a/core/src/main/kotlin/DictionaryCorProcessor.kt b/core/src/main/kotlin/DictionaryCorProcessor.kt index 356271b9..14baa365 100644 --- a/core/src/main/kotlin/DictionaryCorProcessor.kt +++ b/core/src/main/kotlin/DictionaryCorProcessor.kt @@ -2,10 +2,18 @@ package com.gitlab.sszuev.flashcards.core import com.gitlab.sszuev.flashcards.DictionaryContext import com.gitlab.sszuev.flashcards.core.normalizers.normalizers -import com.gitlab.sszuev.flashcards.core.processes.* +import com.gitlab.sszuev.flashcards.core.processes.processCreateDictionary +import com.gitlab.sszuev.flashcards.core.processes.processDeleteDictionary +import com.gitlab.sszuev.flashcards.core.processes.processDownloadDictionary +import com.gitlab.sszuev.flashcards.core.processes.processGetAllDictionary +import com.gitlab.sszuev.flashcards.core.processes.processUploadDictionary import com.gitlab.sszuev.flashcards.core.stubs.dictionaryStubSuccess import com.gitlab.sszuev.flashcards.core.stubs.stubError -import com.gitlab.sszuev.flashcards.core.validators.* +import com.gitlab.sszuev.flashcards.core.validators.validateDictionaryEntityHasNoCardId +import com.gitlab.sszuev.flashcards.core.validators.validateDictionaryId +import com.gitlab.sszuev.flashcards.core.validators.validateDictionaryLangId +import com.gitlab.sszuev.flashcards.core.validators.validateDictionaryResource +import com.gitlab.sszuev.flashcards.core.validators.validateUserId import com.gitlab.sszuev.flashcards.corlib.chain import com.gitlab.sszuev.flashcards.model.domain.DictionaryOperation import com.gitlab.sszuev.flashcards.stubs.stubDictionaries @@ -29,7 +37,6 @@ class DictionaryCorProcessor { validators(DictionaryOperation.GET_ALL_DICTIONARIES) { } runs(DictionaryOperation.GET_ALL_DICTIONARIES) { - processFindUser(DictionaryOperation.GET_ALL_DICTIONARIES) processGetAllDictionary() } } @@ -43,7 +50,6 @@ class DictionaryCorProcessor { validateDictionaryLangId("target-lang") { it.normalizedRequestDictionaryEntity.targetLang.langId } } runs(DictionaryOperation.CREATE_DICTIONARY) { - processFindUser(DictionaryOperation.CREATE_DICTIONARY) processCreateDictionary() } } @@ -55,7 +61,6 @@ class DictionaryCorProcessor { validateDictionaryId { (it as DictionaryContext).normalizedRequestDictionaryId } } runs(DictionaryOperation.DELETE_DICTIONARY) { - processFindUser(DictionaryOperation.DELETE_DICTIONARY) processDeleteDictionary() } } @@ -67,7 +72,6 @@ class DictionaryCorProcessor { validateDictionaryId { (it as DictionaryContext).normalizedRequestDictionaryId } } runs(DictionaryOperation.DOWNLOAD_DICTIONARY) { - processFindUser(DictionaryOperation.DOWNLOAD_DICTIONARY) processDownloadDictionary() } } @@ -79,7 +83,6 @@ class DictionaryCorProcessor { validateDictionaryResource() } runs(DictionaryOperation.UPLOAD_DICTIONARY) { - processFindUser(DictionaryOperation.UPLOAD_DICTIONARY) processUploadDictionary() } } diff --git a/db-common/src/main/kotlin/documents/DocumentCard.kt b/core/src/main/kotlin/documents/DocumentCard.kt similarity index 81% rename from db-common/src/main/kotlin/documents/DocumentCard.kt rename to core/src/main/kotlin/documents/DocumentCard.kt index 7280686e..4c55fd80 100644 --- a/db-common/src/main/kotlin/documents/DocumentCard.kt +++ b/core/src/main/kotlin/documents/DocumentCard.kt @@ -1,4 +1,4 @@ -package com.gitlab.sszuev.flashcards.common.documents +package com.gitlab.sszuev.flashcards.core.documents data class DocumentCard( val text: String, diff --git a/db-common/src/main/kotlin/documents/DocumentCardStatus.kt b/core/src/main/kotlin/documents/DocumentCardStatus.kt similarity index 55% rename from db-common/src/main/kotlin/documents/DocumentCardStatus.kt rename to core/src/main/kotlin/documents/DocumentCardStatus.kt index 7ed403a1..c90b25e1 100644 --- a/db-common/src/main/kotlin/documents/DocumentCardStatus.kt +++ b/core/src/main/kotlin/documents/DocumentCardStatus.kt @@ -1,4 +1,4 @@ -package com.gitlab.sszuev.flashcards.common.documents +package com.gitlab.sszuev.flashcards.core.documents enum class DocumentCardStatus { UNKNOWN, IN_PROCESS, LEARNED diff --git a/db-common/src/main/kotlin/documents/DocumentDictionary.kt b/core/src/main/kotlin/documents/DocumentDictionary.kt similarity index 73% rename from db-common/src/main/kotlin/documents/DocumentDictionary.kt rename to core/src/main/kotlin/documents/DocumentDictionary.kt index 969cbea7..6495249d 100644 --- a/db-common/src/main/kotlin/documents/DocumentDictionary.kt +++ b/core/src/main/kotlin/documents/DocumentDictionary.kt @@ -1,4 +1,4 @@ -package com.gitlab.sszuev.flashcards.common.documents +package com.gitlab.sszuev.flashcards.core.documents data class DocumentDictionary( val name: String, diff --git a/db-common/src/main/kotlin/documents/DocumentReader.kt b/core/src/main/kotlin/documents/DocumentReader.kt similarity index 91% rename from db-common/src/main/kotlin/documents/DocumentReader.kt rename to core/src/main/kotlin/documents/DocumentReader.kt index 28ae28a0..746e7d70 100644 --- a/db-common/src/main/kotlin/documents/DocumentReader.kt +++ b/core/src/main/kotlin/documents/DocumentReader.kt @@ -1,4 +1,4 @@ -package com.gitlab.sszuev.flashcards.common.documents +package com.gitlab.sszuev.flashcards.core.documents import java.io.InputStream diff --git a/db-common/src/main/kotlin/documents/DocumentWriter.kt b/core/src/main/kotlin/documents/DocumentWriter.kt similarity index 93% rename from db-common/src/main/kotlin/documents/DocumentWriter.kt rename to core/src/main/kotlin/documents/DocumentWriter.kt index facbc806..01c18319 100644 --- a/db-common/src/main/kotlin/documents/DocumentWriter.kt +++ b/core/src/main/kotlin/documents/DocumentWriter.kt @@ -1,4 +1,4 @@ -package com.gitlab.sszuev.flashcards.common.documents +package com.gitlab.sszuev.flashcards.core.documents import java.io.ByteArrayOutputStream import java.io.OutputStream diff --git a/core/src/main/kotlin/documents/Documents.kt b/core/src/main/kotlin/documents/Documents.kt new file mode 100644 index 00000000..1127d465 --- /dev/null +++ b/core/src/main/kotlin/documents/Documents.kt @@ -0,0 +1,8 @@ +package com.gitlab.sszuev.flashcards.core.documents + +import com.gitlab.sszuev.flashcards.core.documents.xml.LingvoDocumentReader +import com.gitlab.sszuev.flashcards.core.documents.xml.LingvoDocumentWriter + +fun createReader(): DocumentReader = LingvoDocumentReader() + +fun createWriter(): DocumentWriter = LingvoDocumentWriter() \ No newline at end of file diff --git a/db-common/src/main/kotlin/documents/xml/DOMUtils.kt b/core/src/main/kotlin/documents/xml/DOMUtils.kt similarity index 95% rename from db-common/src/main/kotlin/documents/xml/DOMUtils.kt rename to core/src/main/kotlin/documents/xml/DOMUtils.kt index 26d625f7..36a7f085 100644 --- a/db-common/src/main/kotlin/documents/xml/DOMUtils.kt +++ b/core/src/main/kotlin/documents/xml/DOMUtils.kt @@ -1,6 +1,6 @@ @file:Suppress("MemberVisibilityCanBePrivate") -package com.gitlab.sszuev.flashcards.common.documents.xml +package com.gitlab.sszuev.flashcards.core.documents.xml import org.w3c.dom.Element import org.w3c.dom.Node diff --git a/db-common/src/main/kotlin/documents/xml/LingvoDocumentReader.kt b/core/src/main/kotlin/documents/xml/LingvoDocumentReader.kt similarity index 85% rename from db-common/src/main/kotlin/documents/xml/LingvoDocumentReader.kt rename to core/src/main/kotlin/documents/xml/LingvoDocumentReader.kt index 9128d2ab..ce145459 100644 --- a/db-common/src/main/kotlin/documents/xml/LingvoDocumentReader.kt +++ b/core/src/main/kotlin/documents/xml/LingvoDocumentReader.kt @@ -1,11 +1,11 @@ -package com.gitlab.sszuev.flashcards.common.documents.xml +package com.gitlab.sszuev.flashcards.core.documents.xml -import com.gitlab.sszuev.flashcards.common.documents.DocumentCard -import com.gitlab.sszuev.flashcards.common.documents.DocumentCardStatus -import com.gitlab.sszuev.flashcards.common.documents.DocumentDictionary -import com.gitlab.sszuev.flashcards.common.documents.DocumentReader -import com.gitlab.sszuev.flashcards.common.documents.xml.DOMUtils.element -import com.gitlab.sszuev.flashcards.common.documents.xml.DOMUtils.elements +import com.gitlab.sszuev.flashcards.core.documents.DocumentCard +import com.gitlab.sszuev.flashcards.core.documents.DocumentCardStatus +import com.gitlab.sszuev.flashcards.core.documents.DocumentDictionary +import com.gitlab.sszuev.flashcards.core.documents.DocumentReader +import com.gitlab.sszuev.flashcards.core.documents.xml.DOMUtils.element +import com.gitlab.sszuev.flashcards.core.documents.xml.DOMUtils.elements import org.w3c.dom.Element import org.xml.sax.InputSource import org.xml.sax.SAXException diff --git a/db-common/src/main/kotlin/documents/xml/LingvoDocumentWriter.kt b/core/src/main/kotlin/documents/xml/LingvoDocumentWriter.kt similarity index 93% rename from db-common/src/main/kotlin/documents/xml/LingvoDocumentWriter.kt rename to core/src/main/kotlin/documents/xml/LingvoDocumentWriter.kt index bf76cc18..7a0d82dc 100644 --- a/db-common/src/main/kotlin/documents/xml/LingvoDocumentWriter.kt +++ b/core/src/main/kotlin/documents/xml/LingvoDocumentWriter.kt @@ -1,8 +1,8 @@ -package com.gitlab.sszuev.flashcards.common.documents.xml +package com.gitlab.sszuev.flashcards.core.documents.xml -import com.gitlab.sszuev.flashcards.common.documents.DocumentCard -import com.gitlab.sszuev.flashcards.common.documents.DocumentDictionary -import com.gitlab.sszuev.flashcards.common.documents.DocumentWriter +import com.gitlab.sszuev.flashcards.core.documents.DocumentCard +import com.gitlab.sszuev.flashcards.core.documents.DocumentDictionary +import com.gitlab.sszuev.flashcards.core.documents.DocumentWriter import org.w3c.dom.Document import org.w3c.dom.Element import java.io.OutputStream diff --git a/db-common/src/main/kotlin/documents/xml/LingvoMappings.kt b/core/src/main/kotlin/documents/xml/LingvoMappings.kt similarity index 96% rename from db-common/src/main/kotlin/documents/xml/LingvoMappings.kt rename to core/src/main/kotlin/documents/xml/LingvoMappings.kt index 45428d6c..9e1e5794 100644 --- a/db-common/src/main/kotlin/documents/xml/LingvoMappings.kt +++ b/core/src/main/kotlin/documents/xml/LingvoMappings.kt @@ -1,6 +1,6 @@ -package com.gitlab.sszuev.flashcards.common.documents.xml +package com.gitlab.sszuev.flashcards.core.documents.xml -import com.gitlab.sszuev.flashcards.common.documents.DocumentCardStatus +import com.gitlab.sszuev.flashcards.core.documents.DocumentCardStatus import java.nio.charset.Charset import java.nio.charset.StandardCharsets diff --git a/core/src/main/kotlin/mappers/DbMappers.kt b/core/src/main/kotlin/mappers/DbMappers.kt new file mode 100644 index 00000000..f666c090 --- /dev/null +++ b/core/src/main/kotlin/mappers/DbMappers.kt @@ -0,0 +1,83 @@ +package com.gitlab.sszuev.flashcards.core.mappers + +import com.gitlab.sszuev.flashcards.model.common.AppAuthId +import com.gitlab.sszuev.flashcards.model.domain.CardEntity +import com.gitlab.sszuev.flashcards.model.domain.CardId +import com.gitlab.sszuev.flashcards.model.domain.CardWordEntity +import com.gitlab.sszuev.flashcards.model.domain.CardWordExampleEntity +import com.gitlab.sszuev.flashcards.model.domain.DictionaryEntity +import com.gitlab.sszuev.flashcards.model.domain.DictionaryId +import com.gitlab.sszuev.flashcards.model.domain.LangEntity +import com.gitlab.sszuev.flashcards.model.domain.LangId +import com.gitlab.sszuev.flashcards.model.domain.Stage +import com.gitlab.sszuev.flashcards.repositories.DbCard +import com.gitlab.sszuev.flashcards.repositories.DbDictionary +import com.gitlab.sszuev.flashcards.repositories.DbLang + +fun CardEntity.toDbCard() = DbCard( + cardId = this.cardId.asString(), + dictionaryId = this.dictionaryId.asString(), + answered = this.answered, + changedAt = this.changedAt, + stats = this.stats.mapKeys { it.key.name }, + words = this.words.map { it.toDbCardWord() }, + details = this.details, +) + +fun DbCard.toCardEntity() = CardEntity( + cardId = CardId(this.cardId), + dictionaryId = DictionaryId(this.dictionaryId), + answered = this.answered, + changedAt = this.changedAt, + stats = this.stats.mapKeys { Stage.valueOf(it.key) }, + words = this.words.map { it.toCardWordEntity() }, + details = this.details, +) + +fun DictionaryEntity.toDbDictionary() = DbDictionary( + dictionaryId = dictionaryId.asString(), + name = name, + userId = userId.asString(), + sourceLang = sourceLang.toDbLang(), + targetLang = targetLang.toDbLang(), +) + +fun DbDictionary.toDictionaryEntity() = DictionaryEntity( + dictionaryId = DictionaryId(dictionaryId), + name = name, + userId = AppAuthId(userId), + sourceLang = sourceLang.toLangEntity(), + targetLang = targetLang.toLangEntity(), +) + +private fun CardWordEntity.toDbCardWord() = DbCard.Word( + word = this.word, + transcription = this.transcription, + partOfSpeech = this.partOfSpeech, + examples = this.examples.map { it.toDbCardWordExample() }, + translations = this.translations, +) + +private fun CardWordExampleEntity.toDbCardWordExample() = + DbCard.Word.Example(text = this.text, translation = this.translation) + +private fun DbCard.Word.toCardWordEntity() = CardWordEntity( + word = this.word, + transcription = this.transcription, + partOfSpeech = this.partOfSpeech, + examples = this.examples.map { it.toCardWordExampleEntity() }, + translations = this.translations, +) + +private fun DbCard.Word.Example.toCardWordExampleEntity() = + CardWordExampleEntity(text = this.text, translation = this.translation) + +private fun DbLang.toLangEntity() = LangEntity( + langId = LangId(langId), + partsOfSpeech = partsOfSpeech, +) + +private fun LangEntity.toDbLang() = DbLang( + langId = langId.asString(), + partsOfSpeech = partsOfSpeech, +) \ No newline at end of file diff --git a/core/src/main/kotlin/mappers/DocMappers.kt b/core/src/main/kotlin/mappers/DocMappers.kt new file mode 100644 index 00000000..a2439ba4 --- /dev/null +++ b/core/src/main/kotlin/mappers/DocMappers.kt @@ -0,0 +1,147 @@ +package com.gitlab.sszuev.flashcards.core.mappers + +import com.gitlab.sszuev.flashcards.AppConfig +import com.gitlab.sszuev.flashcards.core.documents.DocumentCard +import com.gitlab.sszuev.flashcards.core.documents.DocumentCardStatus +import com.gitlab.sszuev.flashcards.core.documents.DocumentDictionary +import com.gitlab.sszuev.flashcards.model.domain.CardEntity +import com.gitlab.sszuev.flashcards.model.domain.CardWordEntity +import com.gitlab.sszuev.flashcards.model.domain.CardWordExampleEntity +import com.gitlab.sszuev.flashcards.model.domain.DictionaryEntity +import com.gitlab.sszuev.flashcards.model.domain.LangEntity +import com.gitlab.sszuev.flashcards.model.domain.LangId +import com.gitlab.sszuev.flashcards.repositories.LanguageRepository + +fun DocumentDictionary.toDictionaryEntity(): DictionaryEntity = DictionaryEntity( + name = this.name, + sourceLang = createLangEntity(this.sourceLang), + targetLang = createLangEntity(this.targetLang), +) + +fun DictionaryEntity.toDocumentDictionary(): DocumentDictionary = DocumentDictionary( + name = this.name, + sourceLang = this.sourceLang.langId.asString(), + targetLang = this.targetLang.langId.asString(), + cards = emptyList(), +) + +fun DocumentCard.toCardEntity(config: AppConfig): CardEntity = CardEntity( + words = this.toCardWordEntity(), + details = emptyMap(), + answered = config.answered(this.status), +) + +fun CardEntity.toDocumentCard(config: AppConfig): DocumentCard { + val word = this.words.first() + return DocumentCard( + text = word.word, + transcription = word.transcription, + partOfSpeech = word.partOfSpeech, + translations = word.toDocumentTranslations(), + examples = word.toDocumentExamples(), + status = config.status(this.answered), + ) +} + +internal fun createLangEntity(documentTag: String): LangEntity = LangEntity( + langId = LangId(documentTag), + partsOfSpeech = LanguageRepository.partsOfSpeech(documentTag) +) + +private fun CardWordEntity.toDocumentTranslations(): List = translations.map { it.joinToString(",") } + +private fun CardWordEntity.toDocumentExamples(): List = + examples.map { if (it.translation != null) "${it.text} -- ${it.translation}" else it.text } + +private fun DocumentCard.toCardWordEntity(): List { + val forms = this.text.split(",").map { it.trim() }.filter { it.isNotEmpty() } + val primaryTranslations = this.translations.map { + fromDocumentCardTranslationToCommonWordDtoTranslation(it) + } + val primaryExamples = this.examples.map { example -> + val parts = example.split(" -- ").filter { it.isNotEmpty() } + val (e, t) = if (parts.size == 2) { + parts[0] to parts[1] + } else { + example to null + } + CardWordExampleEntity(text = e, translation = t) + } + return forms.mapIndexed { i, word -> + val examples = if (i == 0) primaryExamples else emptyList() + val translations = if (i == 0) primaryTranslations else emptyList() + val transcription = if (i == 0) this.transcription ?: "" else null + val pos = if (i == 0) this.partOfSpeech ?: "" else null + CardWordEntity( + word = word, + transcription = transcription, + partOfSpeech = pos, + translations = translations, + examples = examples, + ) + } +} + +/** + * Splits the given `phrase` using comma (i.e. '`,`') as separator. + * Commas inside the parentheses (e.g. "`(x,y)`") are not considered. + * + * @param [phrase] + * @return [List] + */ +internal fun fromDocumentCardTranslationToCommonWordDtoTranslation(phrase: String): List { + val parts = phrase.split(",") + val res = mutableListOf() + var i = 0 + while (i < parts.size) { + val pi = parts[i].trim() + if (pi.isEmpty()) { + i++ + continue + } + if (!pi.contains("(") || pi.contains(")")) { + res.add(pi) + i++ + continue + } + val sb = StringBuilder(pi) + var j = i + 1 + while (j < parts.size) { + val pj = parts[j].trim { it <= ' ' } + if (pj.isEmpty()) { + j++ + continue + } + sb.append(", ").append(pj) + if (pj.contains(")")) { + break + } + j++ + } + if (sb.lastIndexOf(")") == -1) { + res.add(pi) + i++ + continue + } + res.add(sb.toString()) + i = j + i++ + } + return res +} + +private fun AppConfig.status(answered: Int?): DocumentCardStatus = if (answered == null) { + DocumentCardStatus.UNKNOWN +} else { + if (answered >= this.numberOfRightAnswers) { + DocumentCardStatus.LEARNED + } else { + DocumentCardStatus.IN_PROCESS + } +} + +private fun AppConfig.answered(status: DocumentCardStatus): Int = when (status) { + DocumentCardStatus.UNKNOWN -> 0 + DocumentCardStatus.IN_PROCESS -> 1 + DocumentCardStatus.LEARNED -> this.numberOfRightAnswers +} \ No newline at end of file diff --git a/core/src/main/kotlin/normalizers/NormalizeWorkers.kt b/core/src/main/kotlin/normalizers/NormalizeWorkers.kt index 5a7f831c..5df03fff 100644 --- a/core/src/main/kotlin/normalizers/NormalizeWorkers.kt +++ b/core/src/main/kotlin/normalizers/NormalizeWorkers.kt @@ -111,7 +111,7 @@ fun CardFilter.normalize(): CardFilter { dictionaryIds = this.dictionaryIds.map { it.normalize() }, random = this.random, length = this.length, - withUnknown = this.withUnknown, + onlyUnknown = this.onlyUnknown, ) } diff --git a/core/src/main/kotlin/processes/AppErrors.kt b/core/src/main/kotlin/processes/AppErrors.kt new file mode 100644 index 00000000..acb4915d --- /dev/null +++ b/core/src/main/kotlin/processes/AppErrors.kt @@ -0,0 +1,93 @@ +package com.gitlab.sszuev.flashcards.core.processes + +import com.gitlab.sszuev.flashcards.model.Id +import com.gitlab.sszuev.flashcards.model.common.AppAuthId +import com.gitlab.sszuev.flashcards.model.common.AppContext +import com.gitlab.sszuev.flashcards.model.common.AppError +import com.gitlab.sszuev.flashcards.model.common.AppOperation +import com.gitlab.sszuev.flashcards.model.common.AppStatus +import com.gitlab.sszuev.flashcards.model.domain.CardId +import com.gitlab.sszuev.flashcards.model.domain.DictionaryId +import com.gitlab.sszuev.flashcards.model.domain.TTSResourceGet + +internal fun TTSResourceGet.toFieldName(): String { + return toString() +} + +internal fun Id.toFieldName(): String { + return asString() +} + +fun forbiddenEntityDataError( + operation: AppOperation, + entityId: Id, + userId: AppAuthId, +) = dataError( + operation = operation, + fieldName = entityId.asString(), + details = when (entityId) { + is DictionaryId -> "access denied: the dictionary (id=${entityId.asString()}) is not owned by the used (id=${userId.asString()})" + is CardId -> "access denied: the card (id=${entityId.asString()}) is not owned by the the used (id=${userId.asString()})" + else -> throw IllegalArgumentException() + }, +) + +fun noDictionaryFoundDataError( + operation: AppOperation, + id: DictionaryId, + userId: AppAuthId, +) = dataError( + operation = operation, + fieldName = id.asString(), + details = """dictionary with id="${id.toFieldName()}" not found for user ${userId.toFieldName()}""" +) + +fun noCardFoundDataError( + operation: AppOperation, + id: CardId, +) = dataError( + operation = operation, + fieldName = id.toFieldName(), + details = """card with id="${id.asString()}" not found""" +) + +fun dataError( + operation: AppOperation, + fieldName: String = "", + details: String = "", + exception: Throwable? = null, +) = AppError( + code = operation.name, + field = fieldName, + group = "data", + message = if (details.isBlank()) "Error while ${operation.name}" else "Error while ${operation.name}: $details", + exception = exception +) + +internal fun AppContext.handleThrowable(operation: AppOperation, ex: Throwable) { + fail( + runError( + operation = operation, + description = "exception", + exception = ex, + ) + ) +} + +internal fun runError( + operation: AppOperation, + fieldName: String = "", + description: String = "", + exception: Throwable? = null, +) = AppError( + code = operation.name, + field = fieldName, + group = "run", + message = if (description.isBlank()) "" else "Error while ${operation.name}: $description", + exception = exception +) + +internal fun AppContext.fail(error: AppError) { + this.status = AppStatus.FAIL + this.errors.add(error) +} diff --git a/core/src/main/kotlin/processes/CardProcessWorkers.kt b/core/src/main/kotlin/processes/CardProcessWorkers.kt index 0af3ce67..fca46ce4 100644 --- a/core/src/main/kotlin/processes/CardProcessWorkers.kt +++ b/core/src/main/kotlin/processes/CardProcessWorkers.kt @@ -1,16 +1,19 @@ package com.gitlab.sszuev.flashcards.core.processes import com.gitlab.sszuev.flashcards.CardContext +import com.gitlab.sszuev.flashcards.core.mappers.toCardEntity +import com.gitlab.sszuev.flashcards.core.mappers.toDbCard +import com.gitlab.sszuev.flashcards.core.mappers.toDictionaryEntity import com.gitlab.sszuev.flashcards.core.normalizers.normalize import com.gitlab.sszuev.flashcards.corlib.ChainDSL import com.gitlab.sszuev.flashcards.corlib.worker import com.gitlab.sszuev.flashcards.model.common.AppStatus +import com.gitlab.sszuev.flashcards.model.domain.CardEntity import com.gitlab.sszuev.flashcards.model.domain.CardOperation +import com.gitlab.sszuev.flashcards.model.domain.DictionaryEntity +import com.gitlab.sszuev.flashcards.model.domain.DictionaryId import com.gitlab.sszuev.flashcards.model.domain.TTSResourceGet import com.gitlab.sszuev.flashcards.model.domain.TTSResourceId -import com.gitlab.sszuev.flashcards.repositories.CardDbResponse -import com.gitlab.sszuev.flashcards.repositories.CardsDbResponse -import com.gitlab.sszuev.flashcards.repositories.RemoveCardDbResponse fun ChainDSL.processGetCard() = worker { this.name = "process get-card request" @@ -18,10 +21,29 @@ fun ChainDSL.processGetCard() = worker { this.status == AppStatus.RUN } process { - val userId = this.contextUserEntity.id + val userId = this.normalizedRequestAppAuthId val cardId = this.normalizedRequestCardEntityId - val res = this.repositories.cardRepository(this.workMode).getCard(userId, cardId) - this.postProcess(res) + val card = this.repositories.cardRepository(this.workMode).findCardById(cardId.asString())?.toCardEntity() + if (card == null) { + this.errors.add(noCardFoundDataError(CardOperation.GET_CARD, cardId)) + } else { + val dictionary = this.repositories.dictionaryRepository(this.workMode) + .findDictionaryById(card.dictionaryId.asString())?.toDictionaryEntity() + if (dictionary == null) { + this.errors.add( + noDictionaryFoundDataError( + CardOperation.GET_CARD, + card.dictionaryId, + normalizedRequestAppAuthId + ) + ) + } else if (dictionary.userId != userId) { + this.errors.add(forbiddenEntityDataError(CardOperation.GET_CARD, card.cardId, userId)) + } else { + this.responseCardEntity = postProcess(card) { dictionary } + } + } + this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN } onException { fail( @@ -41,10 +63,29 @@ fun ChainDSL.processGetAllCards() = worker { this.status == AppStatus.RUN } process { - val userId = this.contextUserEntity.id + val userId = this.normalizedRequestAppAuthId val dictionaryId = this.normalizedRequestDictionaryId - val res = this.repositories.cardRepository(this.workMode).getAllCards(userId, dictionaryId) - this.postProcess(res) + val dictionary = + this.repositories.dictionaryRepository(this.workMode).findDictionaryById(dictionaryId.asString()) + ?.toDictionaryEntity() + if (dictionary == null) { + this.errors.add( + noDictionaryFoundDataError( + CardOperation.GET_ALL_CARDS, + dictionaryId, + normalizedRequestAppAuthId + ) + ) + } else if (dictionary.userId != userId) { + this.errors.add(forbiddenEntityDataError(CardOperation.GET_ALL_CARDS, dictionaryId, userId)) + } else { + val cards = postProcess( + this.repositories.cardRepository(this.workMode) + .findCardsByDictionaryId(dictionaryId.asString()).map { it.toCardEntity() }.iterator() + ) { dictionary } + this.responseCardEntityList = cards + } + this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN } onException { fail( @@ -64,7 +105,24 @@ fun ChainDSL.processCardSearch() = worker { this.status == AppStatus.RUN } process { - this.postProcess(this.findCardDeck()) + val userId = this.normalizedRequestAppAuthId + val found = this.repositories.dictionaryRepository(this.workMode) + .findDictionariesByIdIn(this.normalizedRequestCardFilter.dictionaryIds.map { it.asString() }) + .map { it.toDictionaryEntity() } + .associateBy { it.dictionaryId } + this.normalizedRequestCardFilter.dictionaryIds.filterNot { found.containsKey(it) }.forEach { + this.errors.add(noDictionaryFoundDataError(CardOperation.SEARCH_CARDS, it, normalizedRequestAppAuthId)) + } + found.values.forEach { dictionary -> + if (dictionary.userId != userId) { + this.errors.add(forbiddenEntityDataError(CardOperation.SEARCH_CARDS, dictionary.dictionaryId, userId)) + } + } + if (errors.isEmpty()) { + val cards = postProcess(findCardDeck().iterator()) { checkNotNull(found[it]) } + this.responseCardEntityList = cards + } + this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN } onException { this.handleThrowable(CardOperation.SEARCH_CARDS, it) @@ -77,9 +135,26 @@ fun ChainDSL.processCreateCard() = worker { this.status == AppStatus.RUN } process { - val userId = this.contextUserEntity.id - val res = this.repositories.cardRepository(this.workMode).createCard(userId, this.normalizedRequestCardEntity) - this.postProcess(res) + val userId = this.normalizedRequestAppAuthId + val dictionaryId = this.normalizedRequestCardEntity.dictionaryId + val dictionary = this.repositories.dictionaryRepository(this.workMode) + .findDictionaryById(dictionaryId.asString())?.toDictionaryEntity() + if (dictionary == null) { + this.errors.add( + noDictionaryFoundDataError( + operation = CardOperation.CREATE_CARD, + id = dictionaryId, + userId = normalizedRequestAppAuthId + ) + ) + } else if (dictionary.userId != userId) { + this.errors.add(forbiddenEntityDataError(CardOperation.CREATE_CARD, dictionaryId, userId)) + } else { + val res = this.repositories.cardRepository(this.workMode) + .createCard(this.normalizedRequestCardEntity.toDbCard()).toCardEntity() + this.responseCardEntity = postProcess(res) { dictionary } + } + this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN } onException { this.handleThrowable(CardOperation.CREATE_CARD, it) @@ -92,9 +167,26 @@ fun ChainDSL.processUpdateCard() = worker { this.status == AppStatus.RUN } process { - val userId = this.contextUserEntity.id - val res = this.repositories.cardRepository(this.workMode).updateCard(userId, this.normalizedRequestCardEntity) - this.postProcess(res) + val userId = this.normalizedRequestAppAuthId + val dictionaryId = this.normalizedRequestCardEntity.dictionaryId + val dictionary = this.repositories.dictionaryRepository(this.workMode) + .findDictionaryById(dictionaryId.asString())?.toDictionaryEntity() + if (dictionary == null) { + this.errors.add( + noDictionaryFoundDataError( + operation = CardOperation.UPDATE_CARD, + id = dictionaryId, + userId = normalizedRequestAppAuthId + ) + ) + } else if (dictionary.userId != userId) { + this.errors.add(forbiddenEntityDataError(CardOperation.UPDATE_CARD, dictionaryId, userId)) + } else { + val res = this.repositories.cardRepository(this.workMode) + .updateCard(this.normalizedRequestCardEntity.toDbCard()).toCardEntity() + this.responseCardEntity = postProcess(res) { dictionary } + } + this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN } onException { this.handleThrowable(CardOperation.UPDATE_CARD, it) @@ -107,22 +199,73 @@ fun ChainDSL.processLearnCards() = worker { this.status == AppStatus.RUN } process { - this.postProcess(this.learnCards()) + val userId = this.normalizedRequestAppAuthId + val cardLearns = this.normalizedRequestCardLearnList.associateBy { it.cardId } + val foundCards = this.repositories.cardRepository(this.workMode) + .findCardsByIdIn(cardLearns.keys.map { it.asString() }).map { it.toCardEntity() }.toSet() + val foundCardIds = foundCards.map { it.cardId }.toSet() + val missedCardIds = cardLearns.keys - foundCardIds + missedCardIds.forEach { + errors.add(noCardFoundDataError(CardOperation.LEARN_CARDS, it)) + } + val dictionaryIds = foundCards.map { it.dictionaryId }.toSet() + val foundDictionaries = + this.repositories.dictionaryRepository(this.workMode) + .findDictionariesByIdIn(dictionaryIds.map { it.asString() }) + .map { it.toDictionaryEntity() } + .associateBy { it.dictionaryId } + val missedDictionaries = dictionaryIds - foundDictionaries.keys + missedDictionaries.forEach { + errors.add(noDictionaryFoundDataError(CardOperation.LEARN_CARDS, it, userId)) + } + foundDictionaries.onEach { + if (it.value.userId != userId) { + errors.add(forbiddenEntityDataError(CardOperation.LEARN_CARDS, it.key, userId)) + } + } + if (errors.isEmpty()) { + this.responseCardEntityList = postProcess(learnCards(foundCards, cardLearns).iterator()) { + checkNotNull(foundDictionaries[it]) + } + } + this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN } onException { this.handleThrowable(CardOperation.LEARN_CARDS, it) } } -fun ChainDSL.processResetCards() = worker { +fun ChainDSL.processResetCard() = worker { this.name = "process reset-cards request" test { this.status == AppStatus.RUN } process { - val userId = this.contextUserEntity.id - val res = this.repositories.cardRepository(this.workMode).resetCard(userId, this.normalizedRequestCardEntityId) - this.postProcess(res) + val userId = this.normalizedRequestAppAuthId + val cardId = this.normalizedRequestCardEntityId + val card = this.repositories.cardRepository(this.workMode).findCardById(cardId.asString())?.toCardEntity() + if (card == null) { + this.errors.add(noCardFoundDataError(CardOperation.RESET_CARD, cardId)) + } else { + val dictionaryId = card.dictionaryId + val dictionary = this.repositories.dictionaryRepository(this.workMode) + .findDictionaryById(dictionaryId.asString())?.toDictionaryEntity() + if (dictionary == null) { + this.errors.add( + noDictionaryFoundDataError( + operation = CardOperation.RESET_CARD, + id = dictionaryId, + userId = normalizedRequestAppAuthId + ) + ) + } else if (dictionary.userId != userId) { + this.errors.add(forbiddenEntityDataError(CardOperation.RESET_CARD, dictionaryId, userId)) + } else { + val res = this.repositories.cardRepository(this.workMode).updateCard(card.copy(answered = 0).toDbCard()) + this.responseCardEntity = postProcess(res.toCardEntity()) { dictionary } + } + } + this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN } onException { this.handleThrowable(CardOperation.RESET_CARD, it) @@ -135,55 +278,71 @@ fun ChainDSL.processDeleteCard() = worker { this.status == AppStatus.RUN } process { - val userId = this.contextUserEntity.id - val res = this.repositories.cardRepository(this.workMode).removeCard(userId, this.normalizedRequestCardEntityId) - this.postProcess(res) + val userId = this.normalizedRequestAppAuthId + val cardId = this.normalizedRequestCardEntityId + val card = this.repositories.cardRepository(this.workMode).findCardById(cardId.asString())?.toCardEntity() + if (card == null) { + this.errors.add(noCardFoundDataError(CardOperation.DELETE_CARD, cardId)) + } else { + val dictionary = this.repositories.dictionaryRepository(this.workMode) + .findDictionaryById(card.dictionaryId.asString())?.toDictionaryEntity() + if (dictionary == null) { + this.errors.add( + noDictionaryFoundDataError( + operation = CardOperation.DELETE_CARD, + id = card.dictionaryId, + userId = normalizedRequestAppAuthId + ) + ) + } else if (dictionary.userId != userId) { + this.errors.add(forbiddenEntityDataError(CardOperation.DELETE_CARD, card.cardId, userId)) + } else { + this.repositories.cardRepository(this.workMode) + .deleteCard(this.normalizedRequestCardEntityId.asString()) + } + } + this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN } onException { this.handleThrowable(CardOperation.DELETE_CARD, it) } } -private suspend fun CardContext.postProcess(res: CardsDbResponse) { - check(res != CardsDbResponse.EMPTY) { "Null response" } - this.errors.addAll(res.errors) - val sourceLangByDictionary = res.dictionaries.associate { it.dictionaryId to it.sourceLang.langId } +private suspend fun CardContext.postProcess( + cardsIterator: Iterator, + dictionary: (DictionaryId) -> DictionaryEntity +): List { + val res = mutableListOf() + while (cardsIterator.hasNext()) { + res.add(postProcess(cardsIterator.next(), dictionary)) + } + return res +} + +private suspend fun CardContext.postProcess( + card: CardEntity, + dictionary: (DictionaryId) -> DictionaryEntity +): CardEntity { + check(card != CardEntity.EMPTY) { "Null card" } val tts = this.repositories.ttsClientRepository(this.workMode) - this.responseCardEntityList = res.cards.map { card -> - val sourceLang = sourceLangByDictionary[card.dictionaryId] ?: return@map card - val words = card.words.map { word -> - val wordAudioId = tts.findResourceId(TTSResourceGet(word.word, sourceLang).normalize()) - this.errors.addAll(wordAudioId.errors) - if (wordAudioId.id != TTSResourceId.NONE) { - word.copy(sound = wordAudioId.id) - } else { - word - } - } - val cardAudioString = card.words.joinToString(",") { it.word } - val cardAudioId = tts.findResourceId(TTSResourceGet(cardAudioString, sourceLang).normalize()) - this.errors.addAll(cardAudioId.errors) - val cardSound = if (cardAudioId.id != TTSResourceId.NONE) { - cardAudioId.id + val sourceLang = dictionary.invoke(card.dictionaryId).sourceLang.langId + val words = card.words.map { word -> + val wordAudioId = tts.findResourceId(TTSResourceGet(word.word, sourceLang).normalize()) + this.errors.addAll(wordAudioId.errors) + if (wordAudioId.id != TTSResourceId.NONE) { + word.copy(sound = wordAudioId.id) } else { - TTSResourceId.NONE + word } - card.copy(words = words, sound = cardSound) } - this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN -} -private fun CardContext.postProcess(res: CardDbResponse) { - this.responseCardEntity = res.card - if (res.errors.isNotEmpty()) { - this.errors.addAll(res.errors) + val cardAudioId = if (words.size == 1) { + words.single().sound + } else { + val cardAudioString = card.words.joinToString(",") { it.word } + val findResourceIdResponse = tts.findResourceId(TTSResourceGet(cardAudioString, sourceLang).normalize()) + this.errors.addAll(findResourceIdResponse.errors) + findResourceIdResponse.id } - this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN + return card.copy(words = words, sound = cardAudioId) } - -private fun CardContext.postProcess(res: RemoveCardDbResponse) { - if (res.errors.isNotEmpty()) { - this.errors.addAll(res.errors) - } - this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN -} \ No newline at end of file diff --git a/core/src/main/kotlin/processes/DictionaryProcessWokers.kt b/core/src/main/kotlin/processes/DictionaryProcessWokers.kt index 3da3cc6a..5bd27195 100644 --- a/core/src/main/kotlin/processes/DictionaryProcessWokers.kt +++ b/core/src/main/kotlin/processes/DictionaryProcessWokers.kt @@ -1,11 +1,19 @@ package com.gitlab.sszuev.flashcards.core.processes import com.gitlab.sszuev.flashcards.DictionaryContext -import com.gitlab.sszuev.flashcards.core.validators.fail +import com.gitlab.sszuev.flashcards.core.documents.createReader +import com.gitlab.sszuev.flashcards.core.documents.createWriter +import com.gitlab.sszuev.flashcards.core.mappers.toCardEntity +import com.gitlab.sszuev.flashcards.core.mappers.toDbCard +import com.gitlab.sszuev.flashcards.core.mappers.toDbDictionary +import com.gitlab.sszuev.flashcards.core.mappers.toDictionaryEntity +import com.gitlab.sszuev.flashcards.core.mappers.toDocumentCard +import com.gitlab.sszuev.flashcards.core.mappers.toDocumentDictionary import com.gitlab.sszuev.flashcards.corlib.ChainDSL import com.gitlab.sszuev.flashcards.corlib.worker import com.gitlab.sszuev.flashcards.model.common.AppStatus import com.gitlab.sszuev.flashcards.model.domain.DictionaryOperation +import com.gitlab.sszuev.flashcards.model.domain.ResourceEntity fun ChainDSL.processGetAllDictionary() = worker { this.name = "process get-all-dictionary request" @@ -13,33 +21,24 @@ fun ChainDSL.processGetAllDictionary() = worker { this.status == AppStatus.RUN } process { - val userId = this.contextUserEntity.id - val res = this.repositories.dictionaryRepository(this.workMode).getAllDictionaries(userId) - if (res.errors.isNotEmpty()) { - this.errors.addAll(res.errors) - } - - // TODO: temporary solution - if (res.errors.isEmpty()) { - this.responseDictionaryEntityList = res.dictionaries.map { dictionary -> - val cards = this.repositories.cardRepository(this.workMode).getAllCards(userId, dictionary.dictionaryId) - val total = cards.cards.size - val known = cards.cards.mapNotNull { it.answered }.count { it >= config.numberOfRightAnswers } - dictionary.copy(totalCardsCount = total, learnedCardsCount = known) - } + val userId = this.normalizedRequestAppAuthId + val res = this.repositories.dictionaryRepository(this.workMode) + .findDictionariesByUserId(userId.asString()) + .map { it.toDictionaryEntity() }.toList() + this.responseDictionaryEntityList = res.map { dictionary -> + val cards = + this.repositories.cardRepository(this.workMode) + .findCardsByDictionaryId(dictionary.dictionaryId.asString()) + .toList() + val total = cards.size + val known = cards.mapNotNull { it.answered }.count { it >= config.numberOfRightAnswers } + dictionary.copy(totalCardsCount = total, learnedCardsCount = known) } this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN } onException { - fail( - runError( - operation = DictionaryOperation.GET_ALL_DICTIONARIES, - fieldName = this.contextUserEntity.id.toFieldName(), - description = "exception", - exception = it - ) - ) + this.handleThrowable(DictionaryOperation.GET_ALL_DICTIONARIES, it) } } @@ -49,13 +48,11 @@ fun ChainDSL.processCreateDictionary() = worker { this.status == AppStatus.RUN } process { - val userId = this.contextUserEntity.id - val res = - this.repositories.dictionaryRepository(this.workMode).createDictionary(userId, this.normalizedRequestDictionaryEntity) - this.responseDictionaryEntity = res.dictionary - if (res.errors.isNotEmpty()) { - this.errors.addAll(res.errors) - } + val userId = this.normalizedRequestAppAuthId + val res = this.repositories.dictionaryRepository(this.workMode) + .createDictionary(this.normalizedRequestDictionaryEntity.copy(userId = userId).toDbDictionary()) + .toDictionaryEntity() + this.responseDictionaryEntity = res this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN } onException { @@ -69,11 +66,23 @@ fun ChainDSL.processDeleteDictionary() = worker { this.status == AppStatus.RUN } process { - val userId = this.contextUserEntity.id - val res = - this.repositories.dictionaryRepository(this.workMode).removeDictionary(userId, this.normalizedRequestDictionaryId) - if (res.errors.isNotEmpty()) { - this.errors.addAll(res.errors) + val userId = this.normalizedRequestAppAuthId + val dictionaryId = this.normalizedRequestDictionaryId + val dictionary = this.repositories.dictionaryRepository(this.workMode) + .findDictionaryById(dictionaryId.asString())?.toDictionaryEntity() + if (dictionary == null) { + this.errors.add( + noDictionaryFoundDataError( + operation = DictionaryOperation.DELETE_DICTIONARY, + id = dictionaryId, + userId = normalizedRequestAppAuthId + ) + ) + } else if (dictionary.userId != userId) { + this.errors.add(forbiddenEntityDataError(DictionaryOperation.DELETE_DICTIONARY, dictionaryId, userId)) + } else { + this.repositories.dictionaryRepository(this.workMode) + .deleteDictionary(this.normalizedRequestDictionaryId.asString()) } this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN } @@ -88,12 +97,33 @@ fun ChainDSL.processDownloadDictionary() = worker { this.status == AppStatus.RUN } process { - val userId = this.contextUserEntity.id - val res = - this.repositories.dictionaryRepository(this.workMode).importDictionary(userId, this.normalizedRequestDictionaryId) - this.responseDictionaryResourceEntity = res.resource - if (res.errors.isNotEmpty()) { - this.errors.addAll(res.errors) + val userId = this.normalizedRequestAppAuthId + val dictionaryId = this.normalizedRequestDictionaryId + val dictionary = this.repositories.dictionaryRepository(this.workMode) + .findDictionaryById(dictionaryId.asString())?.toDictionaryEntity() + if (dictionary == null) { + this.errors.add( + noDictionaryFoundDataError( + operation = DictionaryOperation.DOWNLOAD_DICTIONARY, + id = dictionaryId, + userId = normalizedRequestAppAuthId + ) + ) + } else if (dictionary.userId != userId) { + this.errors.add(forbiddenEntityDataError(DictionaryOperation.DOWNLOAD_DICTIONARY, dictionaryId, userId)) + } else { + val cards = this.repositories.cardRepository(this.workMode) + .findCardsByDictionaryId(dictionaryId.asString()) + .map { it.toCardEntity() } + .map { it.toDocumentCard(this.config) } + .toList() + val document = dictionary.toDocumentDictionary().copy(cards = cards) + try { + val res = createWriter().write(document) + this.responseDictionaryResourceEntity = ResourceEntity(resourceId = dictionaryId, data = res) + } catch (ex: Exception) { + handleThrowable(DictionaryOperation.DOWNLOAD_DICTIONARY, ex) + } } this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN } @@ -108,13 +138,25 @@ fun ChainDSL.processUploadDictionary() = worker { this.status == AppStatus.RUN } process { - val res = this.repositories.dictionaryRepository(this.workMode).exportDictionary( - userId = this.contextUserEntity.id, - resource = this.requestDictionaryResourceEntity - ) - this.responseDictionaryEntity = res.dictionary - if (res.errors.isNotEmpty()) { - this.errors.addAll(res.errors) + try { + val userId = this.normalizedRequestAppAuthId + val document = createReader().parse(this.requestDictionaryResourceEntity.data) + val dictionary = this.repositories.dictionaryRepository(this.workMode) + .createDictionary( + document.toDictionaryEntity().copy(userId = userId).toDbDictionary() + ) + .toDictionaryEntity() + val cards = document.cards.asSequence() + .map { it.toCardEntity(this.config) } + .map { it.copy(dictionaryId = dictionary.dictionaryId) } + .map { it.toDbCard() } + .toList() + if (cards.isNotEmpty()) { + this.repositories.cardRepository(this.workMode).createCards(cards) + } + this.responseDictionaryEntity = dictionary + } catch (ex: Exception) { + handleThrowable(DictionaryOperation.UPLOAD_DICTIONARY, ex) } this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN } diff --git a/core/src/main/kotlin/processes/Processes.kt b/core/src/main/kotlin/processes/Processes.kt deleted file mode 100644 index b1b417e7..00000000 --- a/core/src/main/kotlin/processes/Processes.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.gitlab.sszuev.flashcards.core.processes - -import com.gitlab.sszuev.flashcards.model.Id -import com.gitlab.sszuev.flashcards.model.common.AppContext -import com.gitlab.sszuev.flashcards.model.common.AppError -import com.gitlab.sszuev.flashcards.model.common.AppOperation -import com.gitlab.sszuev.flashcards.model.common.AppStatus -import com.gitlab.sszuev.flashcards.model.domain.TTSResourceGet - -internal fun TTSResourceGet.toFieldName(): String { - return toString() -} - -internal fun Id.toFieldName(): String { - return asString() -} - -internal fun runError( - operation: AppOperation, - fieldName: String = "", - description: String = "", - exception: Throwable? = null, -) = AppError( - code = "run::$operation", - field = fieldName, - group = "run", - message = if (description.isBlank()) "" else "Error while $operation: $description", - exception = exception -) - -internal fun AppContext.handleThrowable(operation: AppOperation, ex: Throwable) { - fail( - runError( - operation = operation, - description = "exception", - exception = ex, - ) - ) -} - -internal fun AppContext.fail(error: AppError) { - this.status = AppStatus.FAIL - this.errors.add(error) -} diff --git a/core/src/main/kotlin/processes/SearchCardsHelper.kt b/core/src/main/kotlin/processes/SearchCardsHelper.kt index 775aeb5e..a1c0397f 100644 --- a/core/src/main/kotlin/processes/SearchCardsHelper.kt +++ b/core/src/main/kotlin/processes/SearchCardsHelper.kt @@ -1,9 +1,9 @@ package com.gitlab.sszuev.flashcards.core.processes import com.gitlab.sszuev.flashcards.CardContext +import com.gitlab.sszuev.flashcards.core.mappers.toCardEntity import com.gitlab.sszuev.flashcards.model.domain.CardEntity import com.gitlab.sszuev.flashcards.model.domain.CardWordEntity -import com.gitlab.sszuev.flashcards.repositories.CardsDbResponse private val comparator: Comparator = Comparator { left, right -> val la = left.answered ?: 0 @@ -16,28 +16,29 @@ private val comparator: Comparator = Comparator { left, /** * Prepares a card deck for a tutor-session. */ -fun CardContext.findCardDeck(): CardsDbResponse { - return if (this.normalizedRequestCardFilter.random) { - // For random mode, do not use the database support since logic is quite complicated. - // We load everything into memory, since the dictionary can hardly contain more than a thousand words, - // i.e., this is relatively small data. - val filter = this.normalizedRequestCardFilter.copy(random = false, length = -1) - val res = this.repositories.cardRepository(this.workMode).searchCard(this.contextUserEntity.id, filter) - if (res.errors.isNotEmpty()) { - return res - } - var cards = res.cards.shuffled().sortedWith(comparator) - if (this.normalizedRequestCardFilter.length > 0) { - val set = mutableSetOf() - collectCardDeck(cards, set, this.normalizedRequestCardFilter.length) - cards = set.toList() - cards = cards.shuffled() +internal fun CardContext.findCardDeck(): List { + val threshold = config.numberOfRightAnswers + var cards = this.repositories.cardRepository(this.workMode) + .findCardsByDictionaryIdIn(this.normalizedRequestCardFilter.dictionaryIds.map { it.asString() }) + .filter { !this.normalizedRequestCardFilter.onlyUnknown || (it.answered ?: -1) <= threshold } + .map { it.toCardEntity() } + if (this.normalizedRequestCardFilter.random) { + cards = cards.shuffled() + } + cards = cards.sortedWith(comparator) + if (!this.normalizedRequestCardFilter.random && this.normalizedRequestCardFilter.length > 0) { + return cards.take(this.normalizedRequestCardFilter.length).toList() + } + var res = cards.toList() + if (this.normalizedRequestCardFilter.length > 0) { + val set = mutableSetOf() + collectCardDeck(res, set, this.normalizedRequestCardFilter.length) + res = set.toList() + if (this.normalizedRequestCardFilter.random) { + res = res.shuffled() } - return res.copy(cards = cards) - } else { - this.repositories.cardRepository(this.workMode) - .searchCard(this.contextUserEntity.id, this.normalizedRequestCardFilter) } + return res } private fun collectCardDeck(all: List, res: MutableSet, num: Int) { diff --git a/core/src/main/kotlin/processes/UpdateCardsHelper.kt b/core/src/main/kotlin/processes/UpdateCardsHelper.kt index 0c721c0c..d5f4d030 100644 --- a/core/src/main/kotlin/processes/UpdateCardsHelper.kt +++ b/core/src/main/kotlin/processes/UpdateCardsHelper.kt @@ -1,12 +1,18 @@ package com.gitlab.sszuev.flashcards.core.processes import com.gitlab.sszuev.flashcards.CardContext -import com.gitlab.sszuev.flashcards.repositories.CardsDbResponse +import com.gitlab.sszuev.flashcards.core.mappers.toCardEntity +import com.gitlab.sszuev.flashcards.core.mappers.toDbCard +import com.gitlab.sszuev.flashcards.model.domain.CardEntity +import com.gitlab.sszuev.flashcards.model.domain.CardId +import com.gitlab.sszuev.flashcards.model.domain.CardLearn -fun CardContext.learnCards(): CardsDbResponse { - val cards = this.normalizedRequestCardLearnList.associateBy { it.cardId } - return this.repositories.cardRepository(this.workMode).updateCards(this.contextUserEntity.id, cards.keys) { card -> - val learn = checkNotNull(cards[card.cardId]) +internal fun CardContext.learnCards( + foundCards: Iterable, + cardLearns: Map +): List { + val cards = foundCards.map { card -> + val learn = checkNotNull(cardLearns[card.cardId]) var answered = card.answered?.toLong() ?: 0L val details = card.stats.toMutableMap() learn.details.forEach { @@ -17,6 +23,7 @@ fun CardContext.learnCards(): CardsDbResponse { } details.merge(it.key, it.value) { a, b -> a + b } } - card.copy(stats = details, answered = answered.toInt()) + card.copy(stats = details, answered = answered.toInt()).toDbCard() } + return this.repositories.cardRepository(this.workMode).updateCards(cards).map { it.toCardEntity() } } \ No newline at end of file diff --git a/core/src/main/kotlin/processes/UserProcessWorkers.kt b/core/src/main/kotlin/processes/UserProcessWorkers.kt deleted file mode 100644 index be6f9bda..00000000 --- a/core/src/main/kotlin/processes/UserProcessWorkers.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.gitlab.sszuev.flashcards.core.processes - -import com.gitlab.sszuev.flashcards.core.validators.fail -import com.gitlab.sszuev.flashcards.corlib.ChainDSL -import com.gitlab.sszuev.flashcards.corlib.worker -import com.gitlab.sszuev.flashcards.model.common.AppContext -import com.gitlab.sszuev.flashcards.model.common.AppOperation -import com.gitlab.sszuev.flashcards.model.common.AppStatus - -internal inline fun ChainDSL.processFindUser(operation: AppOperation) = worker { - this.name = "${Context::class.java.simpleName} :: process get-user" - process { - val uid = this.normalizedRequestAppAuthId - val res = this.repositories.userRepository(this.workMode).getUser(uid) - this.contextUserEntity = res.user - if (res.errors.isNotEmpty()) { - this.errors.addAll(res.errors) - } - this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN - } - onException { - fail( - runError( - operation = operation, - fieldName = this.normalizedRequestAppAuthId.toFieldName(), - description = "exception while get user", - exception = it, - ) - ) - } -} \ No newline at end of file diff --git a/core/src/test/kotlin/CardCorProcessorRunCardsTest.kt b/core/src/test/kotlin/CardCorProcessorRunCardsTest.kt index 49b6a870..241695d0 100644 --- a/core/src/test/kotlin/CardCorProcessorRunCardsTest.kt +++ b/core/src/test/kotlin/CardCorProcessorRunCardsTest.kt @@ -1,16 +1,16 @@ package com.gitlab.sszuev.flashcards.core +import com.gitlab.sszuev.flashcards.AppRepositories import com.gitlab.sszuev.flashcards.CardContext -import com.gitlab.sszuev.flashcards.CardRepositories +import com.gitlab.sszuev.flashcards.core.mappers.toDbCard +import com.gitlab.sszuev.flashcards.core.mappers.toDbDictionary import com.gitlab.sszuev.flashcards.dbcommon.mocks.MockDbCardRepository -import com.gitlab.sszuev.flashcards.dbcommon.mocks.MockDbUserRepository +import com.gitlab.sszuev.flashcards.dbcommon.mocks.MockDbDictionaryRepository import com.gitlab.sszuev.flashcards.model.common.AppAuthId import com.gitlab.sszuev.flashcards.model.common.AppError import com.gitlab.sszuev.flashcards.model.common.AppMode import com.gitlab.sszuev.flashcards.model.common.AppRequestId import com.gitlab.sszuev.flashcards.model.common.AppStatus -import com.gitlab.sszuev.flashcards.model.common.AppUserEntity -import com.gitlab.sszuev.flashcards.model.common.AppUserId import com.gitlab.sszuev.flashcards.model.domain.CardEntity import com.gitlab.sszuev.flashcards.model.domain.CardFilter import com.gitlab.sszuev.flashcards.model.domain.CardId @@ -19,14 +19,16 @@ import com.gitlab.sszuev.flashcards.model.domain.CardOperation import com.gitlab.sszuev.flashcards.model.domain.CardWordEntity import com.gitlab.sszuev.flashcards.model.domain.DictionaryId import com.gitlab.sszuev.flashcards.model.domain.Stage -import com.gitlab.sszuev.flashcards.repositories.CardDbResponse -import com.gitlab.sszuev.flashcards.repositories.CardsDbResponse +import com.gitlab.sszuev.flashcards.model.domain.TTSResourceId import com.gitlab.sszuev.flashcards.repositories.DbCardRepository -import com.gitlab.sszuev.flashcards.repositories.DbUserRepository -import com.gitlab.sszuev.flashcards.repositories.RemoveCardDbResponse -import com.gitlab.sszuev.flashcards.repositories.UserEntityDbResponse +import com.gitlab.sszuev.flashcards.repositories.DbDictionaryRepository +import com.gitlab.sszuev.flashcards.repositories.TTSResourceIdResponse +import com.gitlab.sszuev.flashcards.repositories.TTSResourceRepository +import com.gitlab.sszuev.flashcards.speaker.MockTTSResourceRepository import com.gitlab.sszuev.flashcards.stubs.stubCard import com.gitlab.sszuev.flashcards.stubs.stubCards +import com.gitlab.sszuev.flashcards.stubs.stubDictionaries +import com.gitlab.sszuev.flashcards.stubs.stubDictionary import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test @@ -35,21 +37,25 @@ import org.junit.jupiter.params.provider.EnumSource internal class CardCorProcessorRunCardsTest { companion object { - private val testUser = AppUserEntity(AppUserId("42"), AppAuthId("00000000-0000-0000-0000-000000000000")) + private val testUserId = stubDictionary.userId private fun testContext( op: CardOperation, cardRepository: DbCardRepository, - userRepository: DbUserRepository = MockDbUserRepository() + dictionaryRepository: DbDictionaryRepository = MockDbDictionaryRepository(), + ttsResourceRepository: TTSResourceRepository = MockTTSResourceRepository(invokeFindResourceId = { + TTSResourceIdResponse.EMPTY.copy(TTSResourceId(it.lang.asString() + ":" + it.word)) + }), ): CardContext { val context = CardContext( operation = op, - repositories = CardRepositories().copy( - testUserRepository = userRepository, - testCardRepository = cardRepository - ) + repositories = AppRepositories().copy( + testCardRepository = cardRepository, + testDictionaryRepository = dictionaryRepository, + testTTSClientRepository = ttsResourceRepository, + ), ) - context.requestAppAuthId = testUser.authId + context.requestAppAuthId = testUserId context.workMode = AppMode.TEST context.requestId = requestId(op) return context @@ -61,7 +67,7 @@ internal class CardCorProcessorRunCardsTest { private fun assertUnknownError(context: CardContext, op: CardOperation) { val error = assertSingleError(context, op) - Assertions.assertEquals("run::$op", error.code) + Assertions.assertEquals(op.name, error.code) Assertions.assertEquals("run", error.group) Assertions.assertEquals("Error while $op: exception", error.message) Assertions.assertInstanceOf(TestException::class.java, error.exception) @@ -78,55 +84,66 @@ internal class CardCorProcessorRunCardsTest { @Test fun `test get-card success`() = runTest { val testId = CardId("42") - val testResponseEntity = stubCard.copy(cardId = testId) + val testResponseCardEntity = stubCard.copy(cardId = testId) + val testResponseDictionaryEntity = stubDictionary + + var findCardIsCalled = false + var findDictionaryIsCalled = false + val cardRepository = MockDbCardRepository( + invokeFindCardById = { cardId -> + findCardIsCalled = true + if (cardId == testId.asString()) testResponseCardEntity.toDbCard() else null + } + ) - var wasCalled = false - val repository = MockDbCardRepository( - invokeGetCard = { _, cardId -> - wasCalled = true - CardDbResponse(if (cardId == testId) testResponseEntity else CardEntity.EMPTY) + val dictionaryRepository = MockDbDictionaryRepository( + invokeFindDictionaryById = { dictionaryId -> + findDictionaryIsCalled = true + if (dictionaryId == testResponseDictionaryEntity.dictionaryId.asString()) { + testResponseDictionaryEntity.toDbDictionary() + } else { + null + } } ) - val context = testContext(CardOperation.GET_CARD, repository) + val context = testContext( + op = CardOperation.GET_CARD, + cardRepository = cardRepository, + dictionaryRepository = dictionaryRepository, + ) context.requestCardEntityId = testId CardCorProcessor().execute(context) - Assertions.assertTrue(wasCalled) + Assertions.assertTrue(findCardIsCalled) + Assertions.assertTrue(findDictionaryIsCalled) Assertions.assertEquals(requestId(CardOperation.GET_CARD), context.requestId) + Assertions.assertTrue(context.errors.isEmpty()) { context.errors.toString() } Assertions.assertEquals(AppStatus.OK, context.status) - Assertions.assertTrue(context.errors.isEmpty()) - Assertions.assertEquals(testResponseEntity, context.responseCardEntity) + Assertions.assertEquals(testResponseCardEntity, context.responseCardEntity) } @Test fun `test get-card error - unexpected fail`() = runTest { val testCardId = CardId("42") - var getUserWasCalled = false - var getCardWasCalled = false - val cardRepository = MockDbCardRepository(invokeGetCard = { _, _ -> - getCardWasCalled = true - throw TestException() - }) - val userRepository = MockDbUserRepository( - invokeGetUser = { - getUserWasCalled = true - if (it == testUser.authId) UserEntityDbResponse(user = testUser) else throw TestException() + var getIsWasCalled = false + val cardRepository = MockDbCardRepository( + invokeFindCardById = { _ -> + getIsWasCalled = true + throw TestException() } ) val context = - testContext(CardOperation.GET_CARD, cardRepository = cardRepository, userRepository = userRepository) - context.requestAppAuthId = testUser.authId + testContext(op = CardOperation.GET_CARD, cardRepository = cardRepository) context.requestCardEntityId = testCardId CardCorProcessor().execute(context) - Assertions.assertTrue(getUserWasCalled) - Assertions.assertTrue(getCardWasCalled) + Assertions.assertTrue(getIsWasCalled) Assertions.assertEquals(requestId(CardOperation.GET_CARD), context.requestId) assertUnknownError(context, CardOperation.GET_CARD) } @@ -134,50 +151,81 @@ internal class CardCorProcessorRunCardsTest { @Test fun `test get-all-cards success`() = runTest { val testDictionaryId = DictionaryId("42") - val testResponseEntities = stubCards - - var wasCalled = false - val repository = MockDbCardRepository( - invokeGetAllCards = { _, id -> - wasCalled = true - CardsDbResponse(if (id == testDictionaryId) testResponseEntities else emptyList()) + val testDictionary = stubDictionary.copy(dictionaryId = testDictionaryId) + val testCards = stubCards.map { it.copy(dictionaryId = testDictionaryId) } + + var isFindCardsCalled = false + var isFindDictionaryCalled = false + val cardRepository = MockDbCardRepository( + invokeFindCardsByDictionaryId = { id -> + isFindCardsCalled = true + if (id == testDictionaryId.asString()) testCards.map { it.toDbCard() }.asSequence() else emptySequence() + } + ) + val dictionaryRepository = MockDbDictionaryRepository( + invokeFindDictionaryById = { id -> + isFindDictionaryCalled = true + if (id == testDictionaryId.asString()) testDictionary.toDbDictionary() else null } ) - val context = testContext(CardOperation.GET_ALL_CARDS, repository) + val context = testContext( + op = CardOperation.GET_ALL_CARDS, + cardRepository = cardRepository, + dictionaryRepository = dictionaryRepository, + ) context.requestDictionaryId = testDictionaryId CardCorProcessor().execute(context) - Assertions.assertTrue(wasCalled) + Assertions.assertTrue(context.errors.isEmpty()) { context.errors.toString() } + Assertions.assertTrue(isFindCardsCalled) + Assertions.assertTrue(isFindDictionaryCalled) Assertions.assertEquals(requestId(CardOperation.GET_ALL_CARDS), context.requestId) Assertions.assertEquals(AppStatus.OK, context.status) - Assertions.assertTrue(context.errors.isEmpty()) - Assertions.assertEquals(testResponseEntities, context.responseCardEntityList) + Assertions.assertEquals( + testCards.size, + (context.repositories.ttsClientRepository(AppMode.TEST) as MockTTSResourceRepository).findResourceIdCounts.toInt() + ) + + Assertions.assertEquals(testCards, context.responseCardEntityList) } @Test fun `test get-all-cards unexpected fail`() = runTest { val testDictionaryId = DictionaryId("42") + val testDictionary = stubDictionary.copy(dictionaryId = testDictionaryId) val testResponseEntities = stubCards - var wasCalled = false + var isFindCardsCalled = false + var isFindDictionaryCalled = false val repository = MockDbCardRepository( - invokeGetAllCards = { _, id -> - wasCalled = true - CardsDbResponse( - if (id != testDictionaryId) testResponseEntities else throw TestException() - ) + invokeFindCardsByDictionaryId = { id -> + isFindCardsCalled = true + if (id != testDictionaryId.asString()) { + testResponseEntities.map { it.toDbCard() }.asSequence() + } else throw TestException() + } + ) + val dictionaryRepository = MockDbDictionaryRepository( + invokeFindDictionaryById = { id -> + isFindDictionaryCalled = true + if (id == testDictionaryId.asString()) testDictionary.toDbDictionary() else null } ) - val context = testContext(CardOperation.GET_ALL_CARDS, repository) + val context = testContext( + op = CardOperation.GET_ALL_CARDS, + cardRepository = repository, + dictionaryRepository = dictionaryRepository, + ) context.requestDictionaryId = testDictionaryId CardCorProcessor().execute(context) - Assertions.assertTrue(wasCalled) + Assertions.assertTrue(isFindDictionaryCalled) + Assertions.assertTrue(isFindCardsCalled) Assertions.assertEquals(requestId(CardOperation.GET_ALL_CARDS), context.requestId) Assertions.assertEquals(0, context.responseCardEntityList.size) assertUnknownError(context, CardOperation.GET_ALL_CARDS) @@ -185,23 +233,47 @@ internal class CardCorProcessorRunCardsTest { @Test fun `test create-card success`() = runTest { - val testResponseEntity = stubCard.copy(words = listOf(CardWordEntity("HHH"))) - val testRequestEntity = stubCard.copy(words = listOf(CardWordEntity(word = "XXX")), cardId = CardId.NONE) + val testDictionary = stubDictionary + val testResponseEntity = stubCard.copy( + words = listOf(CardWordEntity(word = "HHH", sound = TTSResourceId("sl:HHH"))), + sound = TTSResourceId("sl:HHH") + ) + val testRequestEntity = stubCard.copy( + words = listOf(CardWordEntity(word = "XXX")), + cardId = CardId.NONE, + dictionaryId = DictionaryId("4200") + ) - var wasCalled = false - val repository = MockDbCardRepository( - invokeCreateCard = { _, it -> - wasCalled = true - CardDbResponse(if (it.words == testRequestEntity.words) testResponseEntity else testRequestEntity) + var isCreateCardCalled = false + var isFindDictionaryCalled = false + val cardRepository = MockDbCardRepository( + invokeCreateCard = { card -> + isCreateCardCalled = true + if (card.words.map { it.word } == testRequestEntity.words.map { it.word }) { + testResponseEntity.toDbCard() + } else { + testRequestEntity.toDbCard() + } + } + ) + val dictionaryRepository = MockDbDictionaryRepository( + invokeFindDictionaryById = { id -> + isFindDictionaryCalled = true + if (id == testRequestEntity.dictionaryId.asString()) testDictionary.toDbDictionary() else null } ) - val context = testContext(CardOperation.CREATE_CARD, repository) + val context = testContext( + op = CardOperation.CREATE_CARD, + cardRepository = cardRepository, + dictionaryRepository = dictionaryRepository, + ) context.requestCardEntity = testRequestEntity CardCorProcessor().execute(context) - Assertions.assertTrue(wasCalled) + Assertions.assertTrue(isCreateCardCalled) + Assertions.assertTrue(isFindDictionaryCalled) Assertions.assertEquals(requestId(CardOperation.CREATE_CARD), context.requestId) Assertions.assertEquals(AppStatus.OK, context.status) { "Errors: ${context.errors}" } Assertions.assertTrue(context.errors.isEmpty()) @@ -211,22 +283,39 @@ internal class CardCorProcessorRunCardsTest { @Test fun `test create-card unexpected fail`() = runTest { + val testDictionary = stubDictionary val testRequestEntity = stubCard.copy(words = listOf(CardWordEntity(word = "XXX")), cardId = CardId.NONE) - var wasCalled = false - val repository = MockDbCardRepository( - invokeCreateCard = { _, it -> - wasCalled = true - CardDbResponse(if (it.words == testRequestEntity.words) throw TestException() else testRequestEntity) + var isCreateCardCalled = false + var isFindDictionaryCalled = false + val cardRepository = MockDbCardRepository( + invokeCreateCard = { card -> + isCreateCardCalled = true + if (card.words.map { it.word } == testRequestEntity.words.map { it.word }) { + throw TestException() + } else { + testRequestEntity.toDbCard() + } + } + ) + val dictionaryRepository = MockDbDictionaryRepository( + invokeFindDictionaryById = { id -> + isFindDictionaryCalled = true + if (id == testRequestEntity.dictionaryId.asString()) testDictionary.toDbDictionary() else null } ) - val context = testContext(CardOperation.CREATE_CARD, repository) + val context = testContext( + op = CardOperation.CREATE_CARD, + cardRepository = cardRepository, + dictionaryRepository = dictionaryRepository, + ) context.requestCardEntity = testRequestEntity CardCorProcessor().execute(context) - Assertions.assertTrue(wasCalled) + Assertions.assertTrue(isCreateCardCalled) + Assertions.assertTrue(isFindDictionaryCalled) Assertions.assertEquals(requestId(CardOperation.CREATE_CARD), context.requestId) Assertions.assertEquals(CardEntity.EMPTY, context.responseCardEntity) assertUnknownError(context, CardOperation.CREATE_CARD) @@ -235,32 +324,53 @@ internal class CardCorProcessorRunCardsTest { @Test fun `test search-cards success`() = runTest { val testFilter = CardFilter( - dictionaryIds = listOf(DictionaryId("21"), DictionaryId("42")), + dictionaryIds = listOf(DictionaryId("42")), random = false, - withUnknown = true, + onlyUnknown = true, length = 42, ) - val testResponseEntities = stubCards - - var wasCalled = false - val repository = MockDbCardRepository( - invokeSearchCards = { _, it -> - wasCalled = true - CardsDbResponse(if (it == testFilter) testResponseEntities else emptyList()) + val testCards = stubCards.filter { it.dictionaryId in testFilter.dictionaryIds } + val testDictionaries = stubDictionaries + + var isFindCardsCalled = false + var isFindDictionariesCalled = false + val cardRepository = MockDbCardRepository( + invokeFindCardsByDictionaryIdIn = { ids -> + isFindCardsCalled = true + if (ids == testFilter.dictionaryIds.map { it.asString() }) { + testCards.asSequence().map { it.toDbCard() } + } else { + emptySequence() + } + } + ) + val dictionaryRepository = MockDbDictionaryRepository( + invokeFindDictionariesByIdIn = { givenDictionaryIds -> + isFindDictionariesCalled = true + if (givenDictionaryIds == testFilter.dictionaryIds.map { it.asString() }) { + testDictionaries.asSequence().map { it.toDbDictionary() } + } else { + emptySequence() + } } ) - val context = testContext(CardOperation.SEARCH_CARDS, repository) + val context = testContext( + op = CardOperation.SEARCH_CARDS, + cardRepository = cardRepository, + dictionaryRepository = dictionaryRepository, + ) context.requestCardFilter = testFilter CardCorProcessor().execute(context) - Assertions.assertTrue(wasCalled) + Assertions.assertTrue(isFindCardsCalled) + Assertions.assertTrue(isFindDictionariesCalled) Assertions.assertEquals(requestId(CardOperation.SEARCH_CARDS), context.requestId) Assertions.assertEquals(AppStatus.OK, context.status) Assertions.assertTrue(context.errors.isEmpty()) - Assertions.assertEquals(testResponseEntities, context.responseCardEntityList) + Assertions.assertEquals(testCards, context.responseCardEntityList) } @Test @@ -268,27 +378,46 @@ internal class CardCorProcessorRunCardsTest { val testFilter = CardFilter( dictionaryIds = listOf(DictionaryId("42")), random = false, - withUnknown = false, + onlyUnknown = false, length = 1, ) - val testResponseEntities = stubCards - - var wasCalled = false - val repository = MockDbCardRepository( - invokeSearchCards = { _, it -> - wasCalled = true - CardsDbResponse( - if (it != testFilter) testResponseEntities else throw TestException() - ) + val testDictionaries = stubDictionaries + val testCards = stubCards + + var isFindCardsCalled = false + var isFindDictionariesCalled = false + val cardRepository = MockDbCardRepository( + invokeFindCardsByDictionaryIdIn = { ids -> + isFindCardsCalled = true + if (ids == testFilter.dictionaryIds.map { it.asString() }) { + throw TestException() + } else { + testCards.asSequence().map { it.toDbCard() } + } + } + ) + val dictionaryRepository = MockDbDictionaryRepository( + invokeFindDictionariesByIdIn = { givenDictionaryIds -> + isFindDictionariesCalled = true + if (givenDictionaryIds == testFilter.dictionaryIds.map { it.asString() }) { + testDictionaries.asSequence().map { it.toDbDictionary() } + } else { + emptySequence() + } } ) - val context = testContext(CardOperation.SEARCH_CARDS, repository) + val context = testContext( + op = CardOperation.SEARCH_CARDS, + cardRepository = cardRepository, + dictionaryRepository = dictionaryRepository, + ) context.requestCardFilter = testFilter CardCorProcessor().execute(context) - Assertions.assertTrue(wasCalled) + Assertions.assertTrue(isFindDictionariesCalled) + Assertions.assertTrue(isFindCardsCalled) Assertions.assertEquals(0, context.responseCardEntityList.size) assertUnknownError(context, CardOperation.SEARCH_CARDS) } @@ -296,23 +425,44 @@ internal class CardCorProcessorRunCardsTest { @Test fun `test update-card success`() = runTest { val cardId = CardId("42") + val testDictionary = stubDictionary val testRequestEntity = stubCard.copy(words = listOf(CardWordEntity(word = "XXX")), cardId = cardId) - val testResponseEntity = stubCard.copy(words = listOf(CardWordEntity(word = "HHH"))) - - var wasCalled = false - val repository = MockDbCardRepository( - invokeUpdateCard = { _, it -> - wasCalled = true - CardDbResponse(if (it.cardId == cardId) testResponseEntity else testRequestEntity) + val testResponseEntity = stubCard + + var isUpdateCardCalled = false + var isFindDictionaryCalled = false + val cardRepository = MockDbCardRepository( + invokeUpdateCard = { + isUpdateCardCalled = true + if (it.cardId == cardId.asString()) { + testResponseEntity.toDbCard() + } else { + testRequestEntity.toDbCard() + } + } + ) + val dictionaryRepository = MockDbDictionaryRepository( + invokeFindDictionaryById = { + isFindDictionaryCalled = true + if (testDictionary.dictionaryId.asString() == it) { + testDictionary.toDbDictionary() + } else { + Assertions.fail() + } } ) - val context = testContext(CardOperation.UPDATE_CARD, repository) + val context = testContext( + op = CardOperation.UPDATE_CARD, + cardRepository = cardRepository, + dictionaryRepository = dictionaryRepository, + ) context.requestCardEntity = testRequestEntity CardCorProcessor().execute(context) - Assertions.assertTrue(wasCalled) + Assertions.assertTrue(isUpdateCardCalled) + Assertions.assertTrue(isFindDictionaryCalled) Assertions.assertEquals(requestId(CardOperation.UPDATE_CARD), context.requestId) Assertions.assertEquals(AppStatus.OK, context.status) { "Errors: ${context.errors}" } Assertions.assertTrue(context.errors.isEmpty()) @@ -322,22 +472,44 @@ internal class CardCorProcessorRunCardsTest { @Test fun `test update-card unexpected fail`() = runTest { val cardId = CardId("42") + val testDictionary = stubDictionary val testRequestEntity = stubCard.copy(words = listOf(CardWordEntity(word = "XXX")), cardId = cardId) - var wasCalled = false - val repository = MockDbCardRepository( - invokeUpdateCard = { _, it -> - wasCalled = true - CardDbResponse(if (it.words == testRequestEntity.words) throw TestException() else testRequestEntity) + var isUpdateCardCalled = false + var isFindDictionaryCalled = false + val cardRepository = MockDbCardRepository( + invokeUpdateCard = { + isUpdateCardCalled = true + if (it.cardId == testRequestEntity.cardId.asString()) { + throw TestException() + } else { + testRequestEntity.toDbCard() + } } ) + val dictionaryRepository = MockDbDictionaryRepository( + invokeFindDictionaryById = { + isFindDictionaryCalled = true + if (testDictionary.dictionaryId.asString() == it) { + testDictionary.toDbDictionary() + } else { + Assertions.fail() + } + } + ) + + val context = testContext( + op = CardOperation.UPDATE_CARD, + cardRepository = cardRepository, + dictionaryRepository = dictionaryRepository, + ) - val context = testContext(CardOperation.UPDATE_CARD, repository) context.requestCardEntity = testRequestEntity CardCorProcessor().execute(context) - Assertions.assertTrue(wasCalled) + Assertions.assertTrue(isUpdateCardCalled) + Assertions.assertTrue(isFindDictionaryCalled) Assertions.assertEquals(requestId(CardOperation.UPDATE_CARD), context.requestId) Assertions.assertEquals(CardEntity.EMPTY, context.responseCardEntity) assertUnknownError(context, CardOperation.UPDATE_CARD) @@ -349,28 +521,60 @@ internal class CardCorProcessorRunCardsTest { CardLearn(cardId = stubCard.cardId, details = mapOf(Stage.WRITING to 42)), ) - val testResponseEntities = listOf(stubCard) + val testCards = listOf(stubCard) + val testDictionaries = listOf(stubDictionary) + val expectedCards = listOf( + stubCard.copy( + answered = 42, + stats = stubCard.stats + mapOf(Stage.WRITING to 42) + ) + ) - var wasCalled = false - val repository = MockDbCardRepository( - invokeUpdateCards = { _, givenIds, _ -> - wasCalled = true - CardsDbResponse( - cards = if (givenIds == setOf(stubCard.cardId)) testResponseEntities else emptyList(), - ) + var isUpdateCardsCalled = false + var isFindDictionariesCalled = false + val cardRepository = MockDbCardRepository( + invokeUpdateCards = { givenCards -> + isUpdateCardsCalled = true + if (givenCards.map { it.cardId } == expectedCards.map { it.cardId.asString() }) { + expectedCards.map { it.toDbCard() } + } else { + emptyList() + } + }, + invokeFindCardsByIdIn = { ids -> + if (ids == listOf(stubCard.cardId.asString())) { + testCards.asSequence().map { it.toDbCard() } + } else { + emptySequence() + } + } + ) + val dictionaryRepository = MockDbDictionaryRepository( + invokeFindDictionariesByIdIn = { givenDictionaryIds -> + isFindDictionariesCalled = true + if (testDictionaries.map { it.dictionaryId.asString() } == givenDictionaryIds) { + testDictionaries.asSequence().map { it.toDbDictionary() } + } else { + Assertions.fail() + } } ) - val context = testContext(CardOperation.LEARN_CARDS, repository) + val context = testContext( + op = CardOperation.LEARN_CARDS, + cardRepository = cardRepository, + dictionaryRepository = dictionaryRepository, + ) context.requestCardLearnList = testLearn CardCorProcessor().execute(context) - Assertions.assertTrue(wasCalled) + Assertions.assertTrue(isUpdateCardsCalled) + Assertions.assertTrue(isFindDictionariesCalled) Assertions.assertEquals(requestId(CardOperation.LEARN_CARDS), context.requestId) Assertions.assertEquals(AppStatus.OK, context.status) Assertions.assertTrue(context.errors.isEmpty()) - Assertions.assertEquals(testResponseEntities, context.responseCardEntityList) + Assertions.assertEquals(expectedCards, context.responseCardEntityList) } @Test @@ -379,82 +583,134 @@ internal class CardCorProcessorRunCardsTest { CardLearn(cardId = CardId("1"), details = mapOf(Stage.SELF_TEST to 42)), CardLearn(cardId = CardId("2"), details = mapOf(Stage.OPTIONS to 2, Stage.MOSAIC to 3)) ) - val ids = testLearn.map { it.cardId }.toSet() - - val testResponseEntities = stubCards - val testResponseErrors = listOf( - AppError(code = "test") + val ids = testLearn.map { it.cardId }.map { it.asString() } + + val testCards = listOf(stubCard.copy(cardId = CardId("1")), stubCard.copy(cardId = CardId("2"))) + val testDictionaries = listOf(stubDictionary) + + var isUpdateCardsCalled = false + var isFindDictionariesCalled = false + val cardRepository = MockDbCardRepository( + invokeUpdateCards = { givenCards -> + isUpdateCardsCalled = true + if (givenCards.map { it.cardId } == ids) { + throw TestException() + } else { + emptyList() + } + }, + invokeFindCardsByIdIn = { givenIds -> + if (givenIds == ids) { + testCards.asSequence().map { it.toDbCard() } + } else { + emptySequence() + } + } ) - - var wasCalled = false - val repository = MockDbCardRepository( - invokeUpdateCards = { _, givenIds, _ -> - wasCalled = true - CardsDbResponse( - cards = if (givenIds == ids) testResponseEntities else emptyList(), - errors = if (givenIds == ids) testResponseErrors else emptyList() - ) + val dictionaryRepository = MockDbDictionaryRepository( + invokeFindDictionariesByIdIn = { givenDictionaryIds -> + isFindDictionariesCalled = true + if (testDictionaries.map { it.dictionaryId.asString() } == givenDictionaryIds) { + testDictionaries.asSequence().map { it.toDbDictionary() } + } else { + Assertions.fail() + } } ) - val context = testContext(CardOperation.LEARN_CARDS, repository) + val context = testContext( + op = CardOperation.LEARN_CARDS, + cardRepository = cardRepository, + dictionaryRepository = dictionaryRepository, + ) context.requestCardLearnList = testLearn CardCorProcessor().execute(context) - Assertions.assertTrue(wasCalled) + Assertions.assertTrue(isUpdateCardsCalled) + Assertions.assertTrue(isFindDictionariesCalled) Assertions.assertEquals(requestId(CardOperation.LEARN_CARDS), context.requestId) Assertions.assertEquals(AppStatus.FAIL, context.status) - Assertions.assertEquals(testResponseErrors, context.errors) - Assertions.assertEquals(testResponseEntities, context.responseCardEntityList) + Assertions.assertEquals(emptyList(), context.responseCardEntityList) + + Assertions.assertEquals(1, context.errors.size) + Assertions.assertEquals("LEARN_CARDS", context.errors[0].code) + Assertions.assertInstanceOf(TestException::class.java, context.errors[0].exception) } @Test fun `test reset-card success`() = runTest { - val testId = CardId("42") - val testResponseEntity = stubCard.copy(cardId = testId) - - var wasCalled = false - val repository = MockDbCardRepository( - invokeResetCard = { _, it -> - wasCalled = true - CardDbResponse( - card = if (it == testId) testResponseEntity else CardEntity.EMPTY, - ) + val testDictionaryId = DictionaryId("42") + val testCardId = CardId("42") + val testDictionary = stubDictionary.copy(dictionaryId = testDictionaryId) + val testCard = stubCard.copy(cardId = testCardId, answered = 42, dictionaryId = testDictionaryId) + val expectedCard = testCard.copy(answered = 0) + + var isUpdateCardCalled = false + val cardRepository = MockDbCardRepository( + invokeUpdateCard = { + isUpdateCardCalled = true + if (it.cardId == testCardId.asString()) it else Assertions.fail() + }, + invokeFindCardById = { + if (it == testCardId.asString()) testCard.toDbCard() else null + } + ) + val dictionaryRepository = MockDbDictionaryRepository( + invokeFindDictionaryById = { + if (it == testDictionaryId.asString()) testDictionary.toDbDictionary() else null } ) - val context = testContext(CardOperation.RESET_CARD, repository) - context.requestCardEntityId = testId + val context = testContext( + op = CardOperation.RESET_CARD, + cardRepository = cardRepository, + dictionaryRepository = dictionaryRepository, + ) + context.requestCardEntityId = testCardId CardCorProcessor().execute(context) - Assertions.assertTrue(wasCalled) + Assertions.assertTrue(isUpdateCardCalled) Assertions.assertEquals(requestId(CardOperation.RESET_CARD), context.requestId) Assertions.assertEquals(AppStatus.OK, context.status) Assertions.assertTrue(context.errors.isEmpty()) - Assertions.assertEquals(testResponseEntity, context.responseCardEntity) + Assertions.assertEquals(expectedCard, context.responseCardEntity) } @Test fun `test delete-card success`() = runTest { - val testId = CardId("42") - val response = RemoveCardDbResponse() - - var wasCalled = false - val repository = MockDbCardRepository( - invokeDeleteCard = { _, it -> - wasCalled = true - if (it == testId) response else throw TestException() + val testDictionaryId = DictionaryId("42") + val testCardId = CardId("42") + val testCard = stubCard.copy(cardId = testCardId, dictionaryId = testDictionaryId) + val testDictionary = stubDictionary.copy(dictionaryId = testDictionaryId) + + var isDeleteCardCalled = false + val cardRepository = MockDbCardRepository( + invokeDeleteCard = { + isDeleteCardCalled = true + if (it == testCardId.asString()) testCard.toDbCard() else Assertions.fail() + }, + invokeFindCardById = { + if (it == testCardId.asString()) testCard.toDbCard() else Assertions.fail() + } + ) + val dictionaryRepository = MockDbDictionaryRepository( + invokeFindDictionaryById = { + if (it == testDictionaryId.asString()) testDictionary.toDbDictionary() else Assertions.fail() } ) - val context = testContext(CardOperation.DELETE_CARD, repository) - context.requestCardEntityId = testId + val context = testContext( + op = CardOperation.DELETE_CARD, + cardRepository = cardRepository, + dictionaryRepository = dictionaryRepository, + ) + context.requestCardEntityId = testCardId CardCorProcessor().execute(context) - Assertions.assertTrue(wasCalled) + Assertions.assertTrue(isDeleteCardCalled) Assertions.assertEquals(requestId(CardOperation.DELETE_CARD), context.requestId) Assertions.assertEquals(AppStatus.OK, context.status) Assertions.assertTrue(context.errors.isEmpty()) @@ -462,10 +718,7 @@ internal class CardCorProcessorRunCardsTest { @ParameterizedTest @EnumSource(value = CardOperation::class, names = ["NONE", "GET_RESOURCE"], mode = EnumSource.Mode.EXCLUDE) - fun `test no user found`(op: CardOperation) = runTest { - val testUid = AppAuthId("21") - val testError = AppError(group = "test-error", code = "test-error") - + fun `test no resource found`(op: CardOperation) = runTest { val testCardId = CardId("42") val testDictionaryId = DictionaryId("42") val testLearn = CardLearn(testCardId, mapOf(Stage.SELF_TEST to 42)) @@ -484,19 +737,28 @@ internal class CardCorProcessorRunCardsTest { length = 42, ) - var getUserWasCalled = false - var getCardWasCalled = false - val cardRepository = MockDbCardRepository(invokeGetCard = { _, _ -> - getCardWasCalled = true - throw TestException() - }) - val userRepository = MockDbUserRepository(invokeGetUser = { - getUserWasCalled = true - UserEntityDbResponse(user = AppUserEntity.EMPTY, errors = listOf(testError)) - }) + val expectedError = AppError( + group = "data", + code = op.name, + field = testCardId.asString(), + message = "Error while ${op.name}: dictionary with id=\"${testDictionaryId.asString()}\" " + + "not found for user ${testUserId.asString()}" + ) + + var findCardByIdIsCalled = false + var findCardsByIdInIsCalled = false + val cardRepository = MockDbCardRepository( + invokeFindCardById = { _ -> + findCardByIdIsCalled = true + stubCard.toDbCard() + }, + invokeFindCardsByIdIn = { + findCardsByIdInIsCalled = true + sequenceOf(stubCard.toDbCard()) + } + ) - val context = testContext(op, cardRepository = cardRepository, userRepository = userRepository) - context.requestAppAuthId = testUid + val context = testContext(op, cardRepository = cardRepository) context.requestCardEntityId = testCardId context.requestCardLearnList = listOf(testLearn) context.requestCardEntity = testCardEntity @@ -505,10 +767,25 @@ internal class CardCorProcessorRunCardsTest { CardCorProcessor().execute(context) - Assertions.assertTrue(getUserWasCalled) - Assertions.assertFalse(getCardWasCalled) + when (op) { + CardOperation.NONE, CardOperation.GET_RESOURCE -> Assertions.fail() + CardOperation.SEARCH_CARDS, CardOperation.GET_ALL_CARDS, CardOperation.CREATE_CARD, CardOperation.UPDATE_CARD -> { + Assertions.assertFalse(findCardByIdIsCalled) + Assertions.assertFalse(findCardsByIdInIsCalled) + } + + CardOperation.GET_CARD, CardOperation.DELETE_CARD, CardOperation.RESET_CARD -> { + Assertions.assertTrue(findCardByIdIsCalled) + Assertions.assertFalse(findCardsByIdInIsCalled) + } + + CardOperation.LEARN_CARDS -> { + Assertions.assertFalse(findCardByIdIsCalled) + Assertions.assertTrue(findCardsByIdInIsCalled) + } + } Assertions.assertEquals(requestId(op), context.requestId) val actual = assertSingleError(context, op) - Assertions.assertSame(testError, actual) + Assertions.assertEquals(expectedError, actual) } } \ No newline at end of file diff --git a/core/src/test/kotlin/CardCorProcessorRunResourceTest.kt b/core/src/test/kotlin/CardCorProcessorRunResourceTest.kt index d88aac97..7178093e 100644 --- a/core/src/test/kotlin/CardCorProcessorRunResourceTest.kt +++ b/core/src/test/kotlin/CardCorProcessorRunResourceTest.kt @@ -1,13 +1,16 @@ package com.gitlab.sszuev.flashcards.core +import com.gitlab.sszuev.flashcards.AppRepositories import com.gitlab.sszuev.flashcards.CardContext -import com.gitlab.sszuev.flashcards.CardRepositories -import com.gitlab.sszuev.flashcards.dbcommon.mocks.MockDbUserRepository import com.gitlab.sszuev.flashcards.model.common.AppAuthId import com.gitlab.sszuev.flashcards.model.common.AppMode import com.gitlab.sszuev.flashcards.model.common.AppRequestId import com.gitlab.sszuev.flashcards.model.common.AppStatus -import com.gitlab.sszuev.flashcards.model.domain.* +import com.gitlab.sszuev.flashcards.model.domain.CardOperation +import com.gitlab.sszuev.flashcards.model.domain.LangId +import com.gitlab.sszuev.flashcards.model.domain.ResourceEntity +import com.gitlab.sszuev.flashcards.model.domain.TTSResourceGet +import com.gitlab.sszuev.flashcards.model.domain.TTSResourceId import com.gitlab.sszuev.flashcards.repositories.TTSResourceEntityResponse import com.gitlab.sszuev.flashcards.repositories.TTSResourceIdResponse import com.gitlab.sszuev.flashcards.repositories.TTSResourceRepository @@ -24,8 +27,7 @@ internal class CardCorProcessorRunResourceTest { private fun testContext(repository: TTSResourceRepository): CardContext { val context = CardContext( operation = CardOperation.GET_RESOURCE, - repositories = CardRepositories().copy( - testUserRepository = MockDbUserRepository(), + repositories = AppRepositories().copy( testTTSClientRepository = repository ) ) @@ -114,7 +116,7 @@ internal class CardCorProcessorRunResourceTest { Assertions.assertEquals(ResourceEntity.DUMMY, context.responseTTSResourceEntity) val error = context.errors[0] - Assertions.assertEquals("run::${CardOperation.GET_RESOURCE}", error.code) + Assertions.assertEquals(CardOperation.GET_RESOURCE.name, error.code) Assertions.assertEquals("run", error.group) Assertions.assertEquals(testResourceGet.toString(), error.field) Assertions.assertEquals("Error while GET_RESOURCE: no resource found. filter=${testResourceGet}", error.message) @@ -155,7 +157,7 @@ internal class CardCorProcessorRunResourceTest { Assertions.assertEquals(ResourceEntity.DUMMY, context.responseTTSResourceEntity) val error = context.errors[0] - Assertions.assertEquals("run::${CardOperation.GET_RESOURCE}", error.code) + Assertions.assertEquals(CardOperation.GET_RESOURCE.name, error.code) Assertions.assertEquals("run", error.group) Assertions.assertEquals(testResourceGet.toString(), error.field) Assertions.assertEquals("Error while GET_RESOURCE: unexpected exception", error.message) diff --git a/core/src/test/kotlin/CardCorProcessorStubTest.kt b/core/src/test/kotlin/CardCorProcessorStubTest.kt index 4e9fcb58..a65348d6 100644 --- a/core/src/test/kotlin/CardCorProcessorStubTest.kt +++ b/core/src/test/kotlin/CardCorProcessorStubTest.kt @@ -1,9 +1,25 @@ package com.gitlab.sszuev.flashcards.core import com.gitlab.sszuev.flashcards.CardContext -import com.gitlab.sszuev.flashcards.model.common.* -import com.gitlab.sszuev.flashcards.model.domain.* -import com.gitlab.sszuev.flashcards.stubs.* +import com.gitlab.sszuev.flashcards.model.common.AppError +import com.gitlab.sszuev.flashcards.model.common.AppMode +import com.gitlab.sszuev.flashcards.model.common.AppRequestId +import com.gitlab.sszuev.flashcards.model.common.AppStatus +import com.gitlab.sszuev.flashcards.model.common.AppStub +import com.gitlab.sszuev.flashcards.model.domain.CardEntity +import com.gitlab.sszuev.flashcards.model.domain.CardFilter +import com.gitlab.sszuev.flashcards.model.domain.CardId +import com.gitlab.sszuev.flashcards.model.domain.CardLearn +import com.gitlab.sszuev.flashcards.model.domain.CardOperation +import com.gitlab.sszuev.flashcards.model.domain.DictionaryId +import com.gitlab.sszuev.flashcards.model.domain.LangId +import com.gitlab.sszuev.flashcards.model.domain.Stage +import com.gitlab.sszuev.flashcards.model.domain.TTSResourceGet +import com.gitlab.sszuev.flashcards.stubs.stubAudioResource +import com.gitlab.sszuev.flashcards.stubs.stubCard +import com.gitlab.sszuev.flashcards.stubs.stubCards +import com.gitlab.sszuev.flashcards.stubs.stubError +import com.gitlab.sszuev.flashcards.stubs.stubErrorForCode import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions @@ -12,7 +28,7 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.EnumSource import org.junit.jupiter.params.provider.MethodSource -import java.util.* +import java.util.UUID @OptIn(ExperimentalCoroutinesApi::class) internal class CardCorProcessorStubTest { @@ -27,7 +43,7 @@ internal class CardCorProcessorStubTest { dictionaryIds = listOf(2, 4, 42).map { DictionaryId(it.toString()) }, length = 42, random = true, - withUnknown = false, + onlyUnknown = false, ) private val testCardLearn = CardLearn( cardId = CardId("42"), diff --git a/core/src/test/kotlin/CardCorProcessorValidationTest.kt b/core/src/test/kotlin/CardCorProcessorValidationTest.kt index f36de508..bb40f9c6 100644 --- a/core/src/test/kotlin/CardCorProcessorValidationTest.kt +++ b/core/src/test/kotlin/CardCorProcessorValidationTest.kt @@ -39,7 +39,7 @@ internal class CardCorProcessorValidationTest { dictionaryIds = listOf(4, 2, 42).map { DictionaryId(it.toString()) }, length = 42, random = false, - withUnknown = true, + onlyUnknown = true, ) private val testCardLearn = CardLearn( cardId = CardId("42"), diff --git a/core/src/test/kotlin/DictionaryCorProcessorRunTest.kt b/core/src/test/kotlin/DictionaryCorProcessorRunTest.kt index 39ebe417..957b5909 100644 --- a/core/src/test/kotlin/DictionaryCorProcessorRunTest.kt +++ b/core/src/test/kotlin/DictionaryCorProcessorRunTest.kt @@ -1,29 +1,23 @@ package com.gitlab.sszuev.flashcards.core +import com.gitlab.sszuev.flashcards.AppRepositories import com.gitlab.sszuev.flashcards.DictionaryContext -import com.gitlab.sszuev.flashcards.DictionaryRepositories +import com.gitlab.sszuev.flashcards.core.mappers.toDbCard +import com.gitlab.sszuev.flashcards.core.mappers.toDbDictionary import com.gitlab.sszuev.flashcards.core.normalizers.normalize import com.gitlab.sszuev.flashcards.dbcommon.mocks.MockDbCardRepository import com.gitlab.sszuev.flashcards.dbcommon.mocks.MockDbDictionaryRepository -import com.gitlab.sszuev.flashcards.dbcommon.mocks.MockDbUserRepository -import com.gitlab.sszuev.flashcards.model.common.AppAuthId import com.gitlab.sszuev.flashcards.model.common.AppMode import com.gitlab.sszuev.flashcards.model.common.AppRequestId import com.gitlab.sszuev.flashcards.model.common.AppStatus -import com.gitlab.sszuev.flashcards.model.common.AppUserEntity -import com.gitlab.sszuev.flashcards.model.common.AppUserId import com.gitlab.sszuev.flashcards.model.domain.DictionaryId import com.gitlab.sszuev.flashcards.model.domain.DictionaryOperation +import com.gitlab.sszuev.flashcards.model.domain.LangEntity +import com.gitlab.sszuev.flashcards.model.domain.LangId import com.gitlab.sszuev.flashcards.model.domain.ResourceEntity -import com.gitlab.sszuev.flashcards.repositories.CardsDbResponse import com.gitlab.sszuev.flashcards.repositories.DbCardRepository import com.gitlab.sszuev.flashcards.repositories.DbDictionaryRepository -import com.gitlab.sszuev.flashcards.repositories.DbUserRepository -import com.gitlab.sszuev.flashcards.repositories.DictionariesDbResponse -import com.gitlab.sszuev.flashcards.repositories.DictionaryDbResponse -import com.gitlab.sszuev.flashcards.repositories.ImportDictionaryDbResponse -import com.gitlab.sszuev.flashcards.repositories.RemoveDictionaryDbResponse -import com.gitlab.sszuev.flashcards.repositories.UserEntityDbResponse +import com.gitlab.sszuev.flashcards.stubs.stubCard import com.gitlab.sszuev.flashcards.stubs.stubDictionaries import com.gitlab.sszuev.flashcards.stubs.stubDictionary import kotlinx.coroutines.test.runTest @@ -32,26 +26,22 @@ import org.junit.jupiter.api.Test internal class DictionaryCorProcessorRunTest { companion object { - private val testUser = AppUserEntity(AppUserId("42"), AppAuthId("00000000-0000-0000-0000-000000000000")) + private val testUserId = stubDictionary.userId @Suppress("SameParameterValue") private fun testContext( op: DictionaryOperation, dictionaryRepository: DbDictionaryRepository, - userRepository: DbUserRepository = MockDbUserRepository( - invokeGetUser = { if (it == testUser.authId) UserEntityDbResponse(user = testUser) else throw AssertionError() } - ), cardsRepository: DbCardRepository = MockDbCardRepository(), ): DictionaryContext { val context = DictionaryContext( operation = op, - repositories = DictionaryRepositories().copy( - testUserRepository = userRepository, + repositories = AppRepositories().copy( testDictionaryRepository = dictionaryRepository, testCardRepository = cardsRepository, ) ) - context.requestAppAuthId = testUser.authId + context.requestAppAuthId = testUserId context.workMode = AppMode.TEST context.requestId = requestId(op) return context @@ -69,15 +59,19 @@ internal class DictionaryCorProcessorRunTest { var getAllDictionariesWasCalled = false var getAllCardsWasCalled = false val dictionaryRepository = MockDbDictionaryRepository( - invokeGetAllDictionaries = { + invokeGetAllDictionaries = { userId -> getAllDictionariesWasCalled = true - DictionariesDbResponse(if (it == testUser.id) testResponseEntities else emptyList()) + if (userId == testUserId.asString()) { + testResponseEntities.asSequence().map { it.toDbDictionary() } + } else { + emptySequence() + } } ) val cardsRepository = MockDbCardRepository( - invokeGetAllCards = { _, _ -> + invokeFindCardsByDictionaryId = { _ -> getAllCardsWasCalled = true - CardsDbResponse.EMPTY + emptySequence() } ) @@ -112,9 +106,9 @@ internal class DictionaryCorProcessorRunTest { var wasCalled = false val repository = MockDbDictionaryRepository( - invokeCreateDictionary = { _, d -> + invokeCreateDictionary = { d -> wasCalled = true - DictionaryDbResponse(dictionary = d.copy(testDictionaryId)) + d.copy(dictionaryId = testDictionaryId.asString()) } ) @@ -134,13 +128,16 @@ internal class DictionaryCorProcessorRunTest { @Test fun `test delete-dictionary success`() = runTest { val testId = DictionaryId("42") - val response = RemoveDictionaryDbResponse() + val response = stubDictionary - var wasCalled = false + var isDeleteDictionaryCalled = false val repository = MockDbDictionaryRepository( - invokeDeleteDictionary = { _, it -> - wasCalled = true - if (it == testId) response else throw AssertionError() + invokeFindDictionaryById = { + if (it == testId.asString()) response.toDbDictionary() else Assertions.fail() + }, + invokeDeleteDictionary = { + isDeleteDictionaryCalled = true + if (it == testId.asString()) response.toDbDictionary() else Assertions.fail() } ) @@ -149,7 +146,7 @@ internal class DictionaryCorProcessorRunTest { DictionaryCorProcessor().execute(context) - Assertions.assertTrue(wasCalled) + Assertions.assertTrue(isDeleteDictionaryCalled) Assertions.assertEquals(requestId(DictionaryOperation.DELETE_DICTIONARY), context.requestId) Assertions.assertEquals(AppStatus.OK, context.status) Assertions.assertTrue(context.errors.isEmpty()) @@ -158,50 +155,117 @@ internal class DictionaryCorProcessorRunTest { @Test fun `test download-dictionary success`() = runTest { val testId = DictionaryId("42") - val testData = ResourceEntity(testId, ByteArray(42) { 42 }) - val response = ImportDictionaryDbResponse(resource = testData) + val testDictionary = stubDictionary.copy( + dictionaryId = testId, + sourceLang = LangEntity(LangId("en")), + targetLang = LangEntity(LangId("fr")), + ) + val testCard = stubCard.copy(dictionaryId = testId) - var wasCalled = false - val repository = MockDbDictionaryRepository( - invokeDownloadDictionary = { _, it -> - wasCalled = true - if (it == testId) response else throw AssertionError() + var isFindDictionaryByIdCalled = false + var isFindCardsByDictionaryIdCalled = false + val dictionaryRepository = MockDbDictionaryRepository( + invokeFindDictionaryById = { + isFindDictionaryByIdCalled = true + if (it == testId.asString()) testDictionary.toDbDictionary() else Assertions.fail() + } + ) + val cardsRepository = MockDbCardRepository( + invokeFindCardsByDictionaryId = { + isFindCardsByDictionaryIdCalled = true + if (it == testId.asString()) sequenceOf(testCard.toDbCard()) else Assertions.fail() } ) - val context = testContext(DictionaryOperation.DOWNLOAD_DICTIONARY, repository) + val context = testContext( + op = DictionaryOperation.DOWNLOAD_DICTIONARY, + dictionaryRepository = dictionaryRepository, + cardsRepository = cardsRepository, + ) context.requestDictionaryId = testId DictionaryCorProcessor().execute(context) - Assertions.assertTrue(wasCalled) + Assertions.assertTrue(isFindDictionaryByIdCalled) + Assertions.assertTrue(isFindCardsByDictionaryIdCalled) Assertions.assertEquals(requestId(DictionaryOperation.DOWNLOAD_DICTIONARY), context.requestId) Assertions.assertEquals(AppStatus.OK, context.status) Assertions.assertTrue(context.errors.isEmpty()) + + val document = context.responseDictionaryResourceEntity.data.toString(Charsets.UTF_16) + Assertions.assertTrue(document.contains("")) + Assertions.assertTrue(document.contains("title=\"Stub-dictionary\"")) } @Test fun `test upload-dictionary success`() = runTest { - val testData = ResourceEntity(DictionaryId.NONE, ByteArray(4200) { 42 }) - val response = DictionaryDbResponse(dictionary = stubDictionary) + val testDocument = ResourceEntity.DUMMY.copy( + data = """ + + + + + test + + + + + тест + + + + + + """.trimIndent().toByteArray(Charsets.UTF_16) + ) + val testDictionary = stubDictionary + val testCard = stubCard - var wasCalled = false - val repository = MockDbDictionaryRepository( - invokeUploadDictionary = { id, bytes -> - wasCalled = true - if (id != testUser.id) throw AssertionError() - if (bytes != testData) throw AssertionError() - response + var isCreateDictionaryCalled = false + var isCreateCardsCalled = false + val dictionaryRepository = MockDbDictionaryRepository( + invokeCreateDictionary = { + isCreateDictionaryCalled = true + if (it.name == "test") { + testDictionary.toDbDictionary() + } else { + Assertions.fail() + } + } + ) + val cardsRepository = MockDbCardRepository( + invokeCreateCards = { + isCreateCardsCalled = true + val cards = it.toList() + if (cards.size == 1 && + cards[0].words.single().word == "test" && + cards[0].dictionaryId == testDictionary.dictionaryId.asString() + ) { + listOf(testCard.toDbCard()) + } else { + Assertions.fail() + } } ) - val context = testContext(DictionaryOperation.UPLOAD_DICTIONARY, repository) - context.requestDictionaryResourceEntity = testData + val context = testContext( + op = DictionaryOperation.UPLOAD_DICTIONARY, + dictionaryRepository = dictionaryRepository, + cardsRepository = cardsRepository, + ) + context.requestDictionaryResourceEntity = testDocument DictionaryCorProcessor().execute(context) Assertions.assertTrue(context.errors.isEmpty()) { "errors: ${context.errors}" } - Assertions.assertTrue(wasCalled) + Assertions.assertTrue(isCreateDictionaryCalled) + Assertions.assertTrue(isCreateCardsCalled) Assertions.assertEquals(requestId(DictionaryOperation.UPLOAD_DICTIONARY), context.requestId) Assertions.assertEquals(AppStatus.OK, context.status) } diff --git a/core/src/test/kotlin/DictionaryCorProcessorStubTest.kt b/core/src/test/kotlin/DictionaryCorProcessorStubTest.kt index f055a455..d5dd591e 100644 --- a/core/src/test/kotlin/DictionaryCorProcessorStubTest.kt +++ b/core/src/test/kotlin/DictionaryCorProcessorStubTest.kt @@ -1,22 +1,23 @@ package com.gitlab.sszuev.flashcards.core import com.gitlab.sszuev.flashcards.DictionaryContext -import com.gitlab.sszuev.flashcards.model.common.* +import com.gitlab.sszuev.flashcards.model.common.AppError +import com.gitlab.sszuev.flashcards.model.common.AppMode +import com.gitlab.sszuev.flashcards.model.common.AppRequestId +import com.gitlab.sszuev.flashcards.model.common.AppStatus +import com.gitlab.sszuev.flashcards.model.common.AppStub import com.gitlab.sszuev.flashcards.model.domain.DictionaryOperation import com.gitlab.sszuev.flashcards.stubs.stubDictionaries import com.gitlab.sszuev.flashcards.stubs.stubError -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test -import java.util.* +import java.util.UUID -@OptIn(ExperimentalCoroutinesApi::class) internal class DictionaryCorProcessorStubTest { companion object { private val processor = DictionaryCorProcessor() private val requestId = UUID.randomUUID().toString() - private val testUser = AppUserEntity(AppUserId("42"), AppAuthId("xxx")) @Suppress("SameParameterValue") private fun testContext(op: DictionaryOperation, case: AppStub): DictionaryContext { @@ -43,7 +44,6 @@ internal class DictionaryCorProcessorStubTest { @Test fun `test get-all-dictionary success`() = runTest { val context = testContext(DictionaryOperation.GET_ALL_DICTIONARIES, AppStub.SUCCESS) - context.contextUserEntity = testUser processor.execute(context) assertSuccess(context) Assertions.assertEquals(stubDictionaries, context.responseDictionaryEntityList) @@ -52,7 +52,6 @@ internal class DictionaryCorProcessorStubTest { @Test fun `test get-all-dictionaries error`() = runTest { val context = testContext(DictionaryOperation.GET_ALL_DICTIONARIES, AppStub.UNKNOWN_ERROR) - context.contextUserEntity = testUser processor.execute(context) processor.execute(context) assertFail(context, stubError) diff --git a/db-common/src/test/kotlin/documents/LingvoDocumentTest.kt b/core/src/test/kotlin/documents/LingvoDocumentTest.kt similarity index 92% rename from db-common/src/test/kotlin/documents/LingvoDocumentTest.kt rename to core/src/test/kotlin/documents/LingvoDocumentTest.kt index b3c37607..f732023e 100644 --- a/db-common/src/test/kotlin/documents/LingvoDocumentTest.kt +++ b/core/src/test/kotlin/documents/LingvoDocumentTest.kt @@ -1,11 +1,15 @@ package com.gitlab.sszuev.flashcards.common.documents -import com.gitlab.sszuev.flashcards.common.documents.xml.LingvoDocumentReader -import com.gitlab.sszuev.flashcards.common.documents.xml.LingvoDocumentWriter +import com.gitlab.sszuev.flashcards.core.documents.DocumentCard +import com.gitlab.sszuev.flashcards.core.documents.DocumentCardStatus +import com.gitlab.sszuev.flashcards.core.documents.DocumentDictionary +import com.gitlab.sszuev.flashcards.core.documents.DocumentReader +import com.gitlab.sszuev.flashcards.core.documents.DocumentWriter +import com.gitlab.sszuev.flashcards.core.documents.xml.LingvoDocumentReader +import com.gitlab.sszuev.flashcards.core.documents.xml.LingvoDocumentWriter import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir -import org.slf4j.LoggerFactory import java.io.ByteArrayOutputStream import java.nio.charset.StandardCharsets import java.nio.file.Files @@ -13,7 +17,6 @@ import java.nio.file.Path internal class LingvoDocumentTest { companion object { - private val LOGGER = LoggerFactory.getLogger(LingvoDocumentTest::class.java) private fun normalize(s: String): String { return s.replace("[\n\r\t]".toRegex(), "") @@ -97,7 +100,6 @@ internal class LingvoDocumentTest { val out = ByteArrayOutputStream() createWriter().write(dictionary, out) val txt = out.toString(StandardCharsets.UTF_16) - LOGGER.info("\n{}", txt) val actual = normalize(txt) Assertions.assertEquals(expected, actual) } diff --git a/core/src/test/kotlin/mappers/DocMappersTest.kt b/core/src/test/kotlin/mappers/DocMappersTest.kt new file mode 100644 index 00000000..87916a6f --- /dev/null +++ b/core/src/test/kotlin/mappers/DocMappersTest.kt @@ -0,0 +1,100 @@ +package com.gitlab.sszuev.flashcards.core.mappers + +import com.gitlab.sszuev.flashcards.AppConfig +import com.gitlab.sszuev.flashcards.core.documents.DocumentCard +import com.gitlab.sszuev.flashcards.core.documents.DocumentCardStatus +import com.gitlab.sszuev.flashcards.model.domain.CardEntity +import com.gitlab.sszuev.flashcards.model.domain.CardId +import com.gitlab.sszuev.flashcards.model.domain.CardWordEntity +import com.gitlab.sszuev.flashcards.model.domain.CardWordExampleEntity +import com.gitlab.sszuev.flashcards.model.domain.DictionaryId +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +internal class DocMappersTest { + companion object { + private val testDocumentCard = DocumentCard( + text = "snowfall", + transcription = "ˈsnəʊfɔːl", + partOfSpeech = "noun", + translations = listOf("снегопад"), + examples = listOf( + "Due to the heavy snowfall, all flights have been cancelled... -- Из-за сильного снегопада все рейсы отменены...", + "It's the first snowfall of Christmas.", + ), + status = DocumentCardStatus.LEARNED, + ) + + private val testCardEntity = CardEntity( + details = emptyMap(), + words = listOf( + CardWordEntity( + word = "snowfall", + transcription = "ˈsnəʊfɔːl", + partOfSpeech = "noun", + translations = listOf(listOf("снегопад")), + examples = listOf( + CardWordExampleEntity( + translation = "Из-за сильного снегопада все рейсы отменены...", + text = "Due to the heavy snowfall, all flights have been cancelled...", + ), + CardWordExampleEntity(text = "It's the first snowfall of Christmas.") + ) + ) + ), + answered = 10, + ) + + private fun assertSplitWords(expectedSize: Int, givenString: String) { + val actual1: List = + fromDocumentCardTranslationToCommonWordDtoTranslation(givenString) + Assertions.assertEquals(expectedSize, actual1.size) + actual1.forEach { assertPhrasePart(it) } + Assertions.assertEquals( + expectedSize, fromDocumentCardTranslationToCommonWordDtoTranslation( + givenString + ).size + ) + val actual2: List = + fromDocumentCardTranslationToCommonWordDtoTranslation(givenString) + Assertions.assertEquals(actual1, actual2) + } + + private fun assertPhrasePart(s: String) { + Assertions.assertFalse(s.isEmpty()) + Assertions.assertFalse(s.startsWith(" ")) + Assertions.assertFalse(s.endsWith(" ")) + if (!s.contains("(") || !s.contains(")")) { + Assertions.assertFalse(s.contains(","), "For string '$s'") + } + } + } + + @Test + fun testSplitIntoWords() { + assertSplitWords(0, " ") + assertSplitWords(1, "a. bb.xxx;yyy") + assertSplitWords(6, "a, ew,ewere;errt,&oipuoirwe,ор43ыфю,,,q,,") + assertSplitWords(10, "mmmmmmmm, uuuuuu, uuu (sss, xzxx, aaa), ddd, sss, q, www,ooo , ppp, sss. in zzzzz") + assertSplitWords(3, "s s s s (smth l.), d (smth=d., smth=g) x, (&,?)x y") + } + + @Test + fun `test map document-card to mem-db-card`() { + val givenCard = testDocumentCard.copy(status = DocumentCardStatus.LEARNED) + val actualCard = givenCard.toCardEntity(AppConfig.DEFAULT) + Assertions.assertEquals(testCardEntity, actualCard) + } + + @Test + fun `test map mem-db-card to document-card`() { + val givenCard = testCardEntity.copy( + cardId = CardId("1"), + dictionaryId = DictionaryId("2"), + answered = 42, + details = mapOf("a" to "b"), + ) + val actualCard = givenCard.toDocumentCard(AppConfig.DEFAULT) + Assertions.assertEquals(testDocumentCard, actualCard) + } +} \ No newline at end of file diff --git a/db-common/src/test/resources/documents/IrregularVerbsEnRu.xml b/core/src/test/resources/documents/IrregularVerbsEnRu.xml similarity index 100% rename from db-common/src/test/resources/documents/IrregularVerbsEnRu.xml rename to core/src/test/resources/documents/IrregularVerbsEnRu.xml diff --git a/db-common/src/test/resources/documents/TestDictionaryEnRu.xml b/core/src/test/resources/documents/TestDictionaryEnRu.xml similarity index 100% rename from db-common/src/test/resources/documents/TestDictionaryEnRu.xml rename to core/src/test/resources/documents/TestDictionaryEnRu.xml diff --git a/db-common/src/test/resources/documents/WeatherEnRu.xml b/core/src/test/resources/documents/WeatherEnRu.xml similarity index 100% rename from db-common/src/test/resources/documents/WeatherEnRu.xml rename to core/src/test/resources/documents/WeatherEnRu.xml diff --git a/db-common/build.gradle.kts b/db-common/build.gradle.kts index 0099aa70..c835a396 100644 --- a/db-common/build.gradle.kts +++ b/db-common/build.gradle.kts @@ -11,12 +11,12 @@ dependencies { val slf4jVersion: String by project val typesafeConfigVersion: String by project val jacksonVersion: String by project + val kotlinDatetimeVersion: String by project - implementation(project(":common")) - implementation("com.typesafe:config:$typesafeConfigVersion") implementation("org.slf4j:slf4j-api:$slf4jVersion") implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") + implementation("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinDatetimeVersion") testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitVersion") diff --git a/db-common/src/main/kotlin/CommonErrors.kt b/db-common/src/main/kotlin/CommonErrors.kt deleted file mode 100644 index 0bdd669f..00000000 --- a/db-common/src/main/kotlin/CommonErrors.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.gitlab.sszuev.flashcards.common - -import com.gitlab.sszuev.flashcards.model.Id -import com.gitlab.sszuev.flashcards.model.common.AppAuthId -import com.gitlab.sszuev.flashcards.model.common.AppError -import com.gitlab.sszuev.flashcards.model.common.AppUserId -import com.gitlab.sszuev.flashcards.model.domain.CardId -import com.gitlab.sszuev.flashcards.model.domain.DictionaryId - -fun forbiddenEntityDbError( - operation: String, - entityId: Id, - userId: AppUserId, -): AppError { - return dbError( - operation = operation, - fieldName = entityId.asString(), - details = when (entityId) { - is DictionaryId -> "access denied: the dictionary (id=${entityId.asString()}) is not owned by the used (id=${userId.asString()})" - is CardId -> "access denied: the card (id=${entityId.asString()}) is not owned by the the used (id=${userId.asString()})" - else -> throw IllegalArgumentException() - }, - ) -} - -fun noDictionaryFoundDbError( - operation: String, - id: DictionaryId, -) = dbError( - operation = operation, - fieldName = id.asString(), - details = """dictionary with id="${id.asString()}" not found""" -) - -fun noCardFoundDbError( - operation: String, - id: CardId, -) = dbError(operation = operation, fieldName = id.asString(), details = """card with id="${id.asString()}" not found""") - -fun noUserFoundDbError( - operation: String, - uid: AppAuthId, -) = dbError( - operation = operation, - fieldName = uid.asString(), - details = """user with uid="${uid.asString()}" not found""" -) - -fun wrongUserUuidDbError( - operation: String, - uid: AppAuthId, -) = dbError( - operation = operation, - fieldName = uid.asString(), - details = """wrong uuid="${uid.asString()}"""", -) - -fun wrongResourceDbError(exception: Throwable) = dbError( - operation = "uploadDictionary", - details = """can't parse dictionary from byte-array""", - exception = exception, -) - -fun dbError( - operation: String, - fieldName: String = "", - details: String = "", - exception: Throwable? = null, -) = AppError( - code = "database::$operation", - field = fieldName, - group = "database", - message = if (details.isBlank()) "Error while $operation" else "Error while $operation: $details", - exception = exception -) \ No newline at end of file diff --git a/db-common/src/main/kotlin/DocumentMappers.kt b/db-common/src/main/kotlin/DocumentMappers.kt deleted file mode 100644 index 438fe0f5..00000000 --- a/db-common/src/main/kotlin/DocumentMappers.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.gitlab.sszuev.flashcards.common - -import com.gitlab.sszuev.flashcards.common.documents.DocumentCard -import com.gitlab.sszuev.flashcards.common.documents.DocumentCardStatus - -fun CommonWordDto.toDocumentTranslations(): List { - return translations.map { it.joinToString(",") } -} - -fun CommonWordDto.toDocumentExamples(): List { - return examples.map { if (it.translation != null) "${it.text} -- ${it.translation}" else it.text } -} - -fun DocumentCard.toCommonWordDtoList(): List { - val forms = this.text.split(",").map { it.trim() }.filter { it.isNotEmpty() } - val primaryTranslations = this.translations.map { fromDocumentCardTranslationToCommonWordDtoTranslation(it) } - val primaryExamples = this.examples.map { example -> - val parts = example.split(" -- ").filter { it.isNotEmpty() } - val (e, t) = if (parts.size == 2) { - parts[0] to parts[1] - } else { - example to null - } - CommonExampleDto(text = e, translation = t) - } - return forms.mapIndexed { i, word -> - val examples = if (i == 0) primaryExamples else emptyList() - val translations = if (i == 0) primaryTranslations else emptyList() - val transcription = if (i == 0) this.transcription ?: "" else null - val pos = if (i == 0) this.partOfSpeech ?: "" else null - CommonWordDto( - word = word, - transcription = transcription, - partOfSpeech = pos, - translations = translations, - examples = examples, - ) - } -} - -/** - * Splits the given `phrase` using comma (i.e. '`,`') as separator. - * Commas inside the parentheses (e.g. "`(x,y)`") are not considered. - * - * @param [phrase] - * @return [List] - */ -internal fun fromDocumentCardTranslationToCommonWordDtoTranslation(phrase: String): List { - val parts = phrase.split(",") - val res = mutableListOf() - var i = 0 - while (i < parts.size) { - val pi = parts[i].trim() - if (pi.isEmpty()) { - i++ - continue - } - if (!pi.contains("(") || pi.contains(")")) { - res.add(pi) - i++ - continue - } - val sb = StringBuilder(pi) - var j = i + 1 - while (j < parts.size) { - val pj = parts[j].trim { it <= ' ' } - if (pj.isEmpty()) { - j++ - continue - } - sb.append(", ").append(pj) - if (pj.contains(")")) { - break - } - j++ - } - if (sb.lastIndexOf(")") == -1) { - res.add(pi) - i++ - continue - } - res.add(sb.toString()) - i = j - i++ - } - return res -} - -fun SysConfig.status(answered: Int?): DocumentCardStatus { - return if (answered == null) { - DocumentCardStatus.UNKNOWN - } else { - if (answered >= this.numberOfRightAnswers) { - DocumentCardStatus.LEARNED - } else { - DocumentCardStatus.IN_PROCESS - } - } -} - -fun SysConfig.answered(status: DocumentCardStatus): Int { - return when (status) { - DocumentCardStatus.UNKNOWN -> 0 - DocumentCardStatus.IN_PROCESS -> 1 - DocumentCardStatus.LEARNED -> this.numberOfRightAnswers - } -} \ No newline at end of file diff --git a/db-common/src/main/kotlin/DomainModelMappers.kt b/db-common/src/main/kotlin/DomainModelMappers.kt deleted file mode 100644 index bd24bf36..00000000 --- a/db-common/src/main/kotlin/DomainModelMappers.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.gitlab.sszuev.flashcards.common - -import com.gitlab.sszuev.flashcards.model.Id -import com.gitlab.sszuev.flashcards.model.domain.CardEntity -import com.gitlab.sszuev.flashcards.model.domain.CardId -import com.gitlab.sszuev.flashcards.model.domain.CardLearn -import com.gitlab.sszuev.flashcards.model.domain.CardWordEntity -import com.gitlab.sszuev.flashcards.model.domain.CardWordExampleEntity -import com.gitlab.sszuev.flashcards.model.domain.DictionaryId -import com.gitlab.sszuev.flashcards.model.domain.Stage - -fun validateCardEntityForCreate(entity: CardEntity) { - val errors = mutableListOf() - if (entity.cardId != CardId.NONE) { - errors.add("no card-id specified") - } - errors.addAll(validateCardEntity(entity)) - require(errors.isEmpty()) { - "Card $entity does not pass the validation. Errors = $errors" - } -} - -fun validateCardEntityForUpdate(entity: CardEntity) { - val errors = mutableListOf() - if (entity.cardId == CardId.NONE) { - errors.add("no card-id specified.") - } - errors.addAll(validateCardEntity(entity)) - require(errors.isEmpty()) { - "Card $entity does not pass the validation. Errors = $errors" - } -} - -private fun validateCardEntity(entity: CardEntity): List { - val errors = mutableListOf() - if (entity.dictionaryId == DictionaryId.NONE) { - errors.add("no dictionary-id specified") - } - if (entity.words.isEmpty()) { - errors.add("no words specified") - } - errors.addAll(validateCardWords(entity.words)) - return errors -} - -private fun validateCardWords(words: List): List { - val errors = mutableListOf() - val translations = words.flatMap { it.translations.flatten() }.filter { it.isNotBlank() } - if (translations.isEmpty()) { - errors.add("${words.map { it.word }} :: no translations specified") - } - return errors -} - -fun validateCardLearns(learns: Collection) { - val ids = learns.groupBy { it.cardId }.filter { it.value.size > 1 }.map { it.key } - require(ids.isEmpty()) { "Duplicate card ids: $ids" } -} - -fun CardEntity.wordsAsCommonWordDtoList(): List = words.map { it.toCommonWordDto() } - -fun CardWordEntity.toCommonWordDto(): CommonWordDto = CommonWordDto( - word = this.word, - transcription = this.transcription, - partOfSpeech = this.partOfSpeech, - examples = this.examples.map { it.toCommonExampleDto() }, - translations = this.translations, -) - -fun CommonWordDto.toCardWordEntity(): CardWordEntity = CardWordEntity( - word = word, - transcription = transcription, - translations = translations, - partOfSpeech = partOfSpeech, - examples = examples.map { it.toCardWordExampleEntity() } -) - -private fun CommonExampleDto.toCardWordExampleEntity(): CardWordExampleEntity = CardWordExampleEntity( - text = text, - translation = translation, -) - -fun CardEntity.detailsAsCommonCardDetailsDto(): CommonCardDetailsDto { - return CommonCardDetailsDto(this.details + this.stats.mapKeys { it.key.name }) -} - -fun CardWordExampleEntity.toCommonExampleDto(): CommonExampleDto = CommonExampleDto( - text = this.text, - translation = this.translation, -) - -fun CommonCardDetailsDto.toCardEntityStats(): Map = - this.filterKeys { Stage.entries.map { s -> s.name }.contains(it) } - .mapKeys { Stage.valueOf(it.key) } - .mapValues { it.value.toString().toLong() } - -fun CommonCardDetailsDto.toCardEntityDetails(): Map = - this.filterKeys { !Stage.entries.map { s -> s.name }.contains(it) } - -fun Id.asLong(): Long = if (this.asString().matches("\\d+".toRegex())) { - this.asString().toLong() -} else { - throw IllegalArgumentException("Wrong id specified: $this") -} \ No newline at end of file diff --git a/db-common/src/main/kotlin/SysConfig.kt b/db-common/src/main/kotlin/SysConfig.kt deleted file mode 100644 index 5c16b5aa..00000000 --- a/db-common/src/main/kotlin/SysConfig.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.gitlab.sszuev.flashcards.common - -data class SysConfig( - val numberOfRightAnswers: Int = TutorSettings.numberOfRightAnswers, -) \ No newline at end of file diff --git a/db-common/src/main/kotlin/Timestamps.kt b/db-common/src/main/kotlin/Timestamps.kt new file mode 100644 index 00000000..7c993c56 --- /dev/null +++ b/db-common/src/main/kotlin/Timestamps.kt @@ -0,0 +1,21 @@ +package com.gitlab.sszuev.flashcards + +import kotlinx.datetime.Instant +import kotlinx.datetime.toJavaInstant +import kotlinx.datetime.toKotlinInstant +import java.time.temporal.ChronoUnit + +private val none = Instant.fromEpochMilliseconds(Long.MIN_VALUE) +val Instant.Companion.NONE + get() = none + +fun systemNow(): java.time.LocalDateTime = + java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC).truncatedTo(ChronoUnit.MILLIS).toLocalDateTime() + +fun Instant?.asJava(): java.time.LocalDateTime = + (this ?: Instant.NONE).toJavaInstant().atOffset(java.time.ZoneOffset.UTC).toLocalDateTime() + +fun java.time.LocalDateTime?.asKotlin(): Instant = + this?.toInstant(java.time.ZoneOffset.UTC)?.toKotlinInstant() ?: Instant.NONE + + diff --git a/db-common/src/main/kotlin/TutorSettings.kt b/db-common/src/main/kotlin/TutorSettings.kt deleted file mode 100644 index 19291426..00000000 --- a/db-common/src/main/kotlin/TutorSettings.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.gitlab.sszuev.flashcards.common - -import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory -import org.slf4j.LoggerFactory - -object TutorSettings { - private val logger = LoggerFactory.getLogger(TutorSettings::class.java) - - private val conf: Config = ConfigFactory.load() - - val numberOfRightAnswers = conf.get(key = "app.tutor.run.answers", default = 10) - - init { - logger.info(printDetails()) - } - - private fun printDetails(): String { - return """ - | - |number-of-right-answers = $numberOfRightAnswers - """.replaceIndentByMargin("\t") - } - - private fun Config.get(key: String, default: Int): Int { - return if (hasPath(key)) getInt(key) else default - } - -} \ No newline at end of file diff --git a/db-common/src/main/kotlin/CommonMappers.kt b/db-common/src/main/kotlin/common/CommonMappers.kt similarity index 72% rename from db-common/src/main/kotlin/CommonMappers.kt rename to db-common/src/main/kotlin/common/CommonMappers.kt index 3e72c13f..a9244a19 100644 --- a/db-common/src/main/kotlin/CommonMappers.kt +++ b/db-common/src/main/kotlin/common/CommonMappers.kt @@ -4,18 +4,7 @@ import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule -import com.gitlab.sszuev.flashcards.model.common.NONE -import com.gitlab.sszuev.flashcards.model.domain.Stage -import kotlinx.datetime.toJavaInstant -import kotlinx.datetime.toKotlinInstant -fun systemNow(): java.time.LocalDateTime = java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC).toLocalDateTime() - -fun kotlinx.datetime.Instant?.asJava(): java.time.LocalDateTime = - (this?:kotlinx.datetime.Instant.NONE).toJavaInstant().atOffset(java.time.ZoneOffset.UTC).toLocalDateTime() - -fun java.time.LocalDateTime?.asKotlin(): kotlinx.datetime.Instant = - this?.toInstant(java.time.ZoneOffset.UTC)?.toKotlinInstant() ?: kotlinx.datetime.Instant.NONE private val mapper = ObjectMapper() .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) @@ -56,9 +45,6 @@ fun List.toJsonString(): String { return mapper.writeValueAsString(this) } -fun Map.toCommonCardDtoDetails(): CommonCardDetailsDto = - CommonCardDetailsDto(this.mapKeys { it.key.name }.mapValues { it.value.toString() }) - data class CommonUserDetailsDto(private val content: Map) : Map by content data class CommonDictionaryDetailsDto(private val content: Map) : Map by content diff --git a/db-common/src/main/kotlin/common/DomainModelMappers.kt b/db-common/src/main/kotlin/common/DomainModelMappers.kt new file mode 100644 index 00000000..c9e18da9 --- /dev/null +++ b/db-common/src/main/kotlin/common/DomainModelMappers.kt @@ -0,0 +1,86 @@ +package com.gitlab.sszuev.flashcards.common + +import com.gitlab.sszuev.flashcards.repositories.DbCadStage +import com.gitlab.sszuev.flashcards.repositories.DbCard + +fun validateCardEntityForCreate(entity: DbCard) { + val errors = mutableListOf() + if (entity.cardId.isNotBlank()) { + errors.add("card-id specified") + } + errors.addAll(validateCardEntity(entity)) + require(errors.isEmpty()) { + "Card $entity does not pass the validation. Errors = $errors" + } +} + +fun validateCardEntityForUpdate(entity: DbCard) { + val errors = mutableListOf() + if (entity.cardId.isBlank()) { + errors.add("no card-id specified.") + } + errors.addAll(validateCardEntity(entity)) + require(errors.isEmpty()) { + "Card $entity does not pass the validation. Errors = $errors" + } +} + +private fun validateCardEntity(entity: DbCard): List { + val errors = mutableListOf() + if (entity.dictionaryId.isBlank()) { + errors.add("no dictionary-id specified") + } + if (entity.words.isEmpty()) { + errors.add("no words specified") + } + errors.addAll(validateCardWords(entity.words)) + return errors +} + +private fun validateCardWords(words: List): List { + val errors = mutableListOf() + val translations = words.flatMap { it.translations.flatten() }.filter { it.isNotBlank() } + if (translations.isEmpty()) { + errors.add("${words.map { it.word }} :: no translations specified") + } + return errors +} + +fun DbCard.wordsAsCommonWordDtoList(): List = words.map { it.toCommonWordDto() } + +fun DbCard.Word.toCommonWordDto(): CommonWordDto = CommonWordDto( + word = this.word, + transcription = this.transcription, + partOfSpeech = this.partOfSpeech, + examples = this.examples.map { it.toCommonExampleDto() }, + translations = this.translations, +) + +fun CommonWordDto.toCardWordEntity(): DbCard.Word = DbCard.Word( + word = word, + transcription = transcription, + translations = translations, + partOfSpeech = partOfSpeech, + examples = examples.map { it.toCardWordExampleEntity() } +) + +private fun CommonExampleDto.toCardWordExampleEntity(): DbCard.Word.Example = DbCard.Word.Example( + text = text, + translation = translation, +) + +fun DbCard.detailsAsCommonCardDetailsDto(): CommonCardDetailsDto { + return CommonCardDetailsDto(this.details + this.stats.mapKeys { it.key }) +} + +fun DbCard.Word.Example.toCommonExampleDto(): CommonExampleDto = CommonExampleDto( + text = this.text, + translation = this.translation, +) + +fun CommonCardDetailsDto.toCardEntityStats(): Map = + this.filterKeys { DbCadStage.entries.map { s -> s.name }.contains(it) } + .mapValues { it.value.toString().toLong() } + +fun CommonCardDetailsDto.toCardEntityDetails(): Map = + this.filterKeys { !DbCadStage.entries.map { s -> s.name }.contains(it) } diff --git a/db-common/src/main/kotlin/documents/DocumentLang.kt b/db-common/src/main/kotlin/documents/DocumentLang.kt deleted file mode 100644 index ce91b4d0..00000000 --- a/db-common/src/main/kotlin/documents/DocumentLang.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.gitlab.sszuev.flashcards.common.documents - -data class DocumentLang( - val tag: String, - val partsOfSpeech: List, -) \ No newline at end of file diff --git a/db-common/src/main/kotlin/documents/Documents.kt b/db-common/src/main/kotlin/documents/Documents.kt deleted file mode 100644 index 9770ba51..00000000 --- a/db-common/src/main/kotlin/documents/Documents.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.gitlab.sszuev.flashcards.common.documents - -import com.gitlab.sszuev.flashcards.common.documents.xml.LingvoDocumentReader -import com.gitlab.sszuev.flashcards.common.documents.xml.LingvoDocumentWriter - -fun createReader(): DocumentReader = LingvoDocumentReader() - -fun createWriter(): DocumentWriter = LingvoDocumentWriter() \ No newline at end of file diff --git a/db-common/src/main/kotlin/repositories/DbCadStage.kt b/db-common/src/main/kotlin/repositories/DbCadStage.kt new file mode 100644 index 00000000..33c4f403 --- /dev/null +++ b/db-common/src/main/kotlin/repositories/DbCadStage.kt @@ -0,0 +1,8 @@ +package com.gitlab.sszuev.flashcards.repositories + +enum class DbCadStage { + MOSAIC, + OPTIONS, + WRITING, + SELF_TEST, +} \ No newline at end of file diff --git a/db-common/src/main/kotlin/repositories/DbCard.kt b/db-common/src/main/kotlin/repositories/DbCard.kt new file mode 100644 index 00000000..36e666c7 --- /dev/null +++ b/db-common/src/main/kotlin/repositories/DbCard.kt @@ -0,0 +1,56 @@ +package com.gitlab.sszuev.flashcards.repositories + +import com.gitlab.sszuev.flashcards.NONE +import kotlinx.datetime.Instant + +data class DbCard( + val cardId: String, + val dictionaryId: String, + val words: List, + val stats: Map, + val details: Map, + val answered: Int?, + val changedAt: Instant, +) { + data class Word( + val word: String, + val transcription: String?, + val partOfSpeech: String?, + val examples: List, + val translations: List>, + ) { + data class Example( + val text: String, + val translation: String?, + ) { + companion object { + val NULL = Example( + text = "", + translation = null, + ) + } + } + + companion object { + val NULL = Word( + word = "", + transcription = null, + partOfSpeech = null, + examples = emptyList(), + translations = emptyList(), + ) + } + } + + companion object { + val NULL = DbCard( + cardId = "", + dictionaryId = "", + changedAt = Instant.NONE, + details = emptyMap(), + stats = emptyMap(), + answered = null, + words = emptyList(), + ) + } +} diff --git a/db-common/src/main/kotlin/repositories/DbCardRepository.kt b/db-common/src/main/kotlin/repositories/DbCardRepository.kt new file mode 100644 index 00000000..f4a251b0 --- /dev/null +++ b/db-common/src/main/kotlin/repositories/DbCardRepository.kt @@ -0,0 +1,61 @@ +package com.gitlab.sszuev.flashcards.repositories + +/** + * Database repository to work with cards. + */ +interface DbCardRepository { + + /** + * Finds card by id returning `null` if nothing found. + */ + fun findCardById(cardId: String): DbCard? + + /** + * Finds cards by dictionary id. + */ + fun findCardsByDictionaryId(dictionaryId: String): Sequence + + /** + * Finds cards by dictionary ids. + */ + fun findCardsByDictionaryIdIn(dictionaryIds: Iterable): Sequence = + dictionaryIds.asSequence().flatMap { findCardsByDictionaryId(it) } + + /** + * Finds cards by card ids. + */ + fun findCardsByIdIn(cardIds: Iterable): Sequence = + cardIds.asSequence().mapNotNull { findCardById(it) } + + /** + * Creates a new card returning the corresponding new card record from the db. + * @throws IllegalArgumentException if the specified card has card id or illegal structure + * @throws DbDataException in case card cannot be created for some reason, + * i.e., if the corresponding dictionary does not exist + */ + fun createCard(cardEntity: DbCard): DbCard + + /** + * Performs bulk create. + */ + fun createCards(cardEntities: Iterable): List = cardEntities.map { createCard(it) } + + /** + * Updates the card entity. + * @throws IllegalArgumentException if the specified card has no card id or has illegal structure + * @throws DbDataException in case card cannot be created for some reason, + * i.e., if the corresponding dictionary does not exist + */ + fun updateCard(cardEntity: DbCard): DbCard + + /** + * Performs bulk update. + */ + fun updateCards(cardEntities: Iterable): List = cardEntities.map { updateCard(it) } + + /** + * Deletes the card from the database, returning records that were deleted. + */ + fun deleteCard(cardId: String): DbCard + +} \ No newline at end of file diff --git a/db-common/src/main/kotlin/repositories/DbDataException.kt b/db-common/src/main/kotlin/repositories/DbDataException.kt new file mode 100644 index 00000000..f1ee895a --- /dev/null +++ b/db-common/src/main/kotlin/repositories/DbDataException.kt @@ -0,0 +1,3 @@ +package com.gitlab.sszuev.flashcards.repositories + +class DbDataException(override val message: String, override val cause: Throwable? = null) : Exception(message, cause) \ No newline at end of file diff --git a/db-common/src/main/kotlin/repositories/DbDictionary.kt b/db-common/src/main/kotlin/repositories/DbDictionary.kt new file mode 100644 index 00000000..56bf6f37 --- /dev/null +++ b/db-common/src/main/kotlin/repositories/DbDictionary.kt @@ -0,0 +1,19 @@ +package com.gitlab.sszuev.flashcards.repositories + +data class DbDictionary( + val dictionaryId: String, + val userId: String, + val name: String, + val sourceLang: DbLang, + val targetLang: DbLang, +) { + companion object { + val NULL = DbDictionary( + dictionaryId = "", + userId = "", + name = "", + sourceLang = DbLang.NULL, + targetLang = DbLang.NULL, + ) + } +} \ No newline at end of file diff --git a/db-common/src/main/kotlin/repositories/DbDictionaryRepository.kt b/db-common/src/main/kotlin/repositories/DbDictionaryRepository.kt new file mode 100644 index 00000000..239e8dde --- /dev/null +++ b/db-common/src/main/kotlin/repositories/DbDictionaryRepository.kt @@ -0,0 +1,33 @@ +package com.gitlab.sszuev.flashcards.repositories + +interface DbDictionaryRepository { + /** + * Finds dictionary by id. + */ + fun findDictionaryById(dictionaryId: String): DbDictionary? + + /** + * Finds dictionaries by their id. + */ + fun findDictionariesByIdIn(dictionaryIds: Iterable): Sequence = + dictionaryIds.asSequence().mapNotNull { findDictionaryById(it) } + + /** + * Finds dictionaries by user id. + */ + fun findDictionariesByUserId(userId: String): Sequence + + /** + * Creates dictionary. + * @throws IllegalArgumentException if the specified dictionary has illegal structure + */ + fun createDictionary(entity: DbDictionary): DbDictionary + + /** + * Deletes dictionary by id. + * @throws IllegalArgumentException wrong [dictionaryId] + * @throws DbDataException dictionary not found. + */ + fun deleteDictionary(dictionaryId: String): DbDictionary + +} \ No newline at end of file diff --git a/db-common/src/main/kotlin/repositories/DbLang.kt b/db-common/src/main/kotlin/repositories/DbLang.kt new file mode 100644 index 00000000..6ad93781 --- /dev/null +++ b/db-common/src/main/kotlin/repositories/DbLang.kt @@ -0,0 +1,10 @@ +package com.gitlab.sszuev.flashcards.repositories + +data class DbLang( + val langId: String, + val partsOfSpeech: List = emptyList(), +) { + companion object { + val NULL = DbLang(langId = "", partsOfSpeech = emptyList()) + } +} \ No newline at end of file diff --git a/db-common/src/main/kotlin/LanguageRepository.kt b/db-common/src/main/kotlin/repositories/LanguageRepository.kt similarity index 93% rename from db-common/src/main/kotlin/LanguageRepository.kt rename to db-common/src/main/kotlin/repositories/LanguageRepository.kt index 99293e4b..136b0f7c 100644 --- a/db-common/src/main/kotlin/LanguageRepository.kt +++ b/db-common/src/main/kotlin/repositories/LanguageRepository.kt @@ -1,4 +1,4 @@ -package com.gitlab.sszuev.flashcards.common +package com.gitlab.sszuev.flashcards.repositories import java.util.Locale import java.util.Objects diff --git a/db-common/src/main/kotlin/repositories/NoOpDbCardRepository.kt b/db-common/src/main/kotlin/repositories/NoOpDbCardRepository.kt new file mode 100644 index 00000000..26788a47 --- /dev/null +++ b/db-common/src/main/kotlin/repositories/NoOpDbCardRepository.kt @@ -0,0 +1,24 @@ +package com.gitlab.sszuev.flashcards.repositories + +object NoOpDbCardRepository : DbCardRepository { + + override fun findCardById(cardId: String): DbCard = noOp() + + override fun findCardsByDictionaryId(dictionaryId: String): Sequence = noOp() + + override fun findCardsByDictionaryIdIn(dictionaryIds: Iterable): Sequence = noOp() + + override fun findCardsByIdIn(cardIds: Iterable): Sequence = noOp() + + override fun createCard(cardEntity: DbCard): DbCard = noOp() + + override fun updateCard(cardEntity: DbCard): DbCard = noOp() + + override fun updateCards(cardEntities: Iterable): List = noOp() + + override fun deleteCard(cardId: String): DbCard = noOp() + + private fun noOp(): Nothing { + error("Must not be called.") + } +} \ No newline at end of file diff --git a/db-common/src/main/kotlin/repositories/NoOpDbDictionaryRepository.kt b/db-common/src/main/kotlin/repositories/NoOpDbDictionaryRepository.kt new file mode 100644 index 00000000..74d96d56 --- /dev/null +++ b/db-common/src/main/kotlin/repositories/NoOpDbDictionaryRepository.kt @@ -0,0 +1,15 @@ +package com.gitlab.sszuev.flashcards.repositories + +object NoOpDbDictionaryRepository : DbDictionaryRepository { + override fun findDictionaryById(dictionaryId: String): DbDictionary = noOp() + + override fun findDictionariesByUserId(userId: String): Sequence = noOp() + + override fun createDictionary(entity: DbDictionary): DbDictionary = noOp() + + override fun deleteDictionary(dictionaryId: String): DbDictionary = noOp() + + private fun noOp(): Nothing { + error("Must not be called.") + } +} \ No newline at end of file diff --git a/db-common/src/main/resources/application.properties b/db-common/src/main/resources/application.properties deleted file mode 100644 index 16e5f1d5..00000000 --- a/db-common/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -app.tutor.run.answers=10 \ No newline at end of file diff --git a/db-common/src/test/kotlin/EntityMappersTest.kt b/db-common/src/test/kotlin/EntityMappersTest.kt deleted file mode 100644 index ee0fe9bd..00000000 --- a/db-common/src/test/kotlin/EntityMappersTest.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.gitlab.sszuev.flashcards.common - -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test - -internal class EntityMappersTest { - - companion object { - private fun assertSplitWords(expectedSize: Int, givenString: String) { - val actual1: List = fromDocumentCardTranslationToCommonWordDtoTranslation(givenString) - Assertions.assertEquals(expectedSize, actual1.size) - actual1.forEach { assertPhrasePart(it) } - Assertions.assertEquals(expectedSize, fromDocumentCardTranslationToCommonWordDtoTranslation(givenString).size) - val actual2: List = fromDocumentCardTranslationToCommonWordDtoTranslation(givenString) - Assertions.assertEquals(actual1, actual2) - } - - private fun assertPhrasePart(s: String) { - Assertions.assertFalse(s.isEmpty()) - Assertions.assertFalse(s.startsWith(" ")) - Assertions.assertFalse(s.endsWith(" ")) - if (!s.contains("(") || !s.contains(")")) { - Assertions.assertFalse(s.contains(","), "For string '$s'") - } - } - } - - @Test - fun testSplitIntoWords() { - assertSplitWords(0, " ") - assertSplitWords(1, "a. bb.xxx;yyy") - assertSplitWords(6, "a, ew,ewere;errt,&oipuoirwe,ор43ыфю,,,q,,") - assertSplitWords(10, "mmmmmmmm, uuuuuu, uuu (sss, xzxx, aaa), ddd, sss, q, www,ooo , ppp, sss. in zzzzz") - assertSplitWords(3, "s s s s (smth l.), d (smth=d., smth=g) x, (&,?)x y") - } -} \ No newline at end of file diff --git a/db-common/src/testFixtures/kotlin/DbCardRepositoryTest.kt b/db-common/src/testFixtures/kotlin/DbCardRepositoryTest.kt index 00a1735d..a60a11cf 100644 --- a/db-common/src/testFixtures/kotlin/DbCardRepositoryTest.kt +++ b/db-common/src/testFixtures/kotlin/DbCardRepositoryTest.kt @@ -1,23 +1,20 @@ package com.gitlab.sszuev.flashcards.dbcommon -import com.gitlab.sszuev.flashcards.common.asLong -import com.gitlab.sszuev.flashcards.model.common.AppError -import com.gitlab.sszuev.flashcards.model.common.AppUserId +import com.gitlab.sszuev.flashcards.asKotlin import com.gitlab.sszuev.flashcards.model.common.NONE -import com.gitlab.sszuev.flashcards.model.domain.CardEntity -import com.gitlab.sszuev.flashcards.model.domain.CardFilter -import com.gitlab.sszuev.flashcards.model.domain.CardId -import com.gitlab.sszuev.flashcards.model.domain.CardWordEntity -import com.gitlab.sszuev.flashcards.model.domain.CardWordExampleEntity -import com.gitlab.sszuev.flashcards.model.domain.DictionaryId -import com.gitlab.sszuev.flashcards.model.domain.LangId import com.gitlab.sszuev.flashcards.model.domain.Stage -import com.gitlab.sszuev.flashcards.repositories.CardDbResponse +import com.gitlab.sszuev.flashcards.repositories.DbCard import com.gitlab.sszuev.flashcards.repositories.DbCardRepository -import com.gitlab.sszuev.flashcards.repositories.RemoveCardDbResponse -import kotlinx.datetime.Clock +import com.gitlab.sszuev.flashcards.repositories.DbDataException +import com.gitlab.sszuev.flashcards.systemNow import kotlinx.datetime.Instant import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNotSame +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.MethodOrderer import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test @@ -26,230 +23,205 @@ import org.junit.jupiter.api.TestMethodOrder /** * Note: all implementations must have the same ids in tests for the same entities to have deterministic behavior. */ -@Suppress("FunctionName") @TestMethodOrder(MethodOrderer.OrderAnnotation::class) abstract class DbCardRepositoryTest { abstract val repository: DbCardRepository companion object { - private val userId = AppUserId("42") - private val drawCardEntity = CardEntity( - cardId = CardId("38"), - dictionaryId = DictionaryId("1"), + private val drawCardEntity = DbCard( + cardId = "38", + dictionaryId = "1", + changedAt = Instant.NONE, + details = emptyMap(), + stats = emptyMap(), + answered = null, words = listOf( - CardWordEntity( + DbCard.Word.NULL.copy( word = "draw", partOfSpeech = "verb", translations = listOf(listOf("рисовать"), listOf("чертить")), examples = emptyList(), ), - CardWordEntity( + DbCard.Word.NULL.copy( word = "drew", ), - CardWordEntity( + DbCard.Word.NULL.copy( word = "drawn", ), ), ) - private val forgiveCardEntity = CardEntity( - cardId = CardId("58"), - dictionaryId = DictionaryId("1"), + private val forgiveCardEntity = DbCard( + cardId = "58", + dictionaryId = "1", + changedAt = Instant.NONE, + details = emptyMap(), + stats = emptyMap(), + answered = null, words = listOf( - CardWordEntity( + DbCard.Word.NULL.copy( word = "forgive", partOfSpeech = "verb", translations = listOf(listOf("прощать")), examples = emptyList(), ), - CardWordEntity( + DbCard.Word.NULL.copy( word = "forgave", ), - CardWordEntity( + DbCard.Word.NULL.copy( word = "forgiven", ), ), ) - private val weatherCardEntity = CardEntity( - cardId = CardId("246"), - dictionaryId = DictionaryId("2"), + private val weatherCardEntity = DbCard( + cardId = "246", + dictionaryId = "2", + changedAt = Instant.NONE, + details = emptyMap(), + stats = emptyMap(), + answered = null, words = listOf( - CardWordEntity( + DbCard.Word( word = "weather", transcription = "'weðə", partOfSpeech = "noun", translations = listOf(listOf("погода")), examples = listOf( - CardWordExampleEntity(text = "weather forecast", translation = "прогноз погоды"), - CardWordExampleEntity(text = "weather bureau", translation = "бюро погоды"), - CardWordExampleEntity(text = "nasty weather", translation = "ненастная погода"), - CardWordExampleEntity(text = "spell of cold weather", translation = "похолодание"), + DbCard.Word.Example(text = "weather forecast", translation = "прогноз погоды"), + DbCard.Word.Example(text = "weather bureau", translation = "бюро погоды"), + DbCard.Word.Example(text = "nasty weather", translation = "ненастная погода"), + DbCard.Word.Example(text = "spell of cold weather", translation = "похолодание"), ), ), ), ) - private val climateCardEntity = CardEntity( + private val climateCardEntity = DbCard( cardId = weatherCardEntity.cardId, - dictionaryId = DictionaryId("2"), + dictionaryId = "2", + changedAt = Instant.NONE, + details = emptyMap(), + stats = mapOf(Stage.SELF_TEST.name to 3), + answered = null, words = listOf( - CardWordEntity( + DbCard.Word.NULL.copy( word = "climate", transcription = "ˈklaɪmɪt", partOfSpeech = "noun", translations = listOf(listOf("климат", "атмосфера", "обстановка"), listOf("климатические условия")), examples = listOf( - CardWordExampleEntity("Create a climate of fear, and it's easy to keep the borders closed."), - CardWordExampleEntity("The clock of climate change is ticking in these magnificent landscapes."), + DbCard.Word.Example.NULL.copy(text = "Create a climate of fear, and it's easy to keep the borders closed."), + DbCard.Word.Example.NULL.copy(text = "The clock of climate change is ticking in these magnificent landscapes."), ), ), ), - stats = mapOf(Stage.SELF_TEST to 3), ) - private val snowCardEntity = CardEntity( - cardId = CardId("247"), - dictionaryId = DictionaryId("2"), + private val snowCardEntity = DbCard( + cardId = "247", + dictionaryId = "2", + changedAt = Instant.NONE, + details = emptyMap(), + stats = emptyMap(), + answered = null, words = listOf( - CardWordEntity( + DbCard.Word.NULL.copy( word = "snow", transcription = "snəu", partOfSpeech = "noun", translations = listOf(listOf("снег")), examples = listOf( - CardWordExampleEntity(text = "It snows.", translation = "Идет снег."), - CardWordExampleEntity(text = "a flake of snow", translation = "снежинка"), - CardWordExampleEntity(text = "snow depth", translation = "высота снежного покрова"), + DbCard.Word.Example(text = "It snows.", translation = "Идет снег."), + DbCard.Word.Example(text = "a flake of snow", translation = "снежинка"), + DbCard.Word.Example(text = "snow depth", translation = "высота снежного покрова"), ), ), ), ) - private val newMurkyCardEntity = CardEntity( - dictionaryId = DictionaryId("2"), + private val newMurkyCardEntity = DbCard( + cardId = "", + dictionaryId = "2", + changedAt = Instant.NONE, + details = emptyMap(), + stats = mapOf(Stage.OPTIONS.name to 0), + answered = 42, words = listOf( - CardWordEntity( + DbCard.Word.NULL.copy( word = "murky", transcription = "ˈmɜːkɪ", partOfSpeech = "adjective", translations = listOf(listOf("темный"), listOf("пасмурный")), - examples = listOf(CardWordExampleEntity("Well, that's a murky issue, isn't it?")), + examples = listOf(DbCard.Word.Example.NULL.copy(text = "Well, that's a murky issue, isn't it?")), ), ), - stats = mapOf(Stage.OPTIONS to 0), - answered = 42, ) @Suppress("SameParameterValue") private fun assertCard( - expected: CardEntity, - actual: CardEntity, + expected: DbCard, + actual: DbCard, ignoreChangeAt: Boolean = true, ignoreId: Boolean = false ) { - Assertions.assertNotSame(expected, actual) + assertNotSame(expected, actual) var a = actual if (ignoreId) { - Assertions.assertNotEquals(CardId.NONE, actual.cardId) - a = a.copy(cardId = CardId.NONE) + assertNotEquals("", actual.cardId) + a = a.copy(cardId = "") } else { - Assertions.assertEquals(expected.cardId, actual.cardId) + assertEquals(expected.cardId, actual.cardId) } if (ignoreChangeAt) { - Assertions.assertNotEquals(Instant.NONE, actual.changedAt) + assertNotEquals(Instant.NONE, actual.changedAt) a = a.copy(changedAt = Instant.NONE) } else { - Assertions.assertEquals(expected.changedAt, actual.changedAt) + assertEquals(expected.changedAt, actual.changedAt) } - Assertions.assertNotEquals(Instant.NONE, actual.changedAt) - Assertions.assertEquals(expected, a) - } - - private fun assertSingleError(res: CardDbResponse, field: String, op: String): AppError { - Assertions.assertEquals(1, res.errors.size) { "Errors: ${res.errors}" } - val error = res.errors[0] - Assertions.assertEquals("database::$op", error.code) { error.toString() } - Assertions.assertEquals(field, error.field) { error.toString() } - Assertions.assertEquals("database", error.group) { error.toString() } - Assertions.assertNull(error.exception) { error.toString() } - return error - } - - private fun assertNoErrors(res: CardDbResponse) { - Assertions.assertEquals(0, res.errors.size) { "Has errors: ${res.errors}" } - } - - private fun assertNoErrors(res: RemoveCardDbResponse) { - Assertions.assertEquals(0, res.errors.size) { "Has errors: ${res.errors}" } + assertNotEquals(Instant.NONE, actual.changedAt) + assertEquals(expected, a) } } @Test - fun `test get card error unknown card`() { - val id = CardId("42000") - val res = repository.getCard(userId, id) - Assertions.assertEquals(CardEntity.EMPTY, res.card) - Assertions.assertEquals(1, res.errors.size) - val error = res.errors[0] - Assertions.assertEquals("database::getCard", error.code) - Assertions.assertEquals(id.asString(), error.field) - Assertions.assertEquals("database", error.group) - Assertions.assertEquals( - """Error while getCard: card with id="${id.asString()}" not found""", - error.message - ) - Assertions.assertNull(error.exception) + fun `test get card not found`() { + val id = "42000" + val res = repository.findCardById(id) + assertNull(res) } @Order(1) @Test fun `test get all cards success`() { // Business dictionary - val res1 = repository.getAllCards(userId, DictionaryId("1")) - Assertions.assertEquals(244, res1.cards.size) - Assertions.assertEquals(0, res1.errors.size) - Assertions.assertEquals(1, res1.dictionaries.size) - Assertions.assertEquals("1", res1.dictionaries.single().dictionaryId.asString()) + val res1 = repository.findCardsByDictionaryId("1").toList() + assertEquals(244, res1.size) + assertEquals("1", res1.map { it.dictionaryId }.toSet().single()) // Weather dictionary - val res2 = repository.getAllCards(userId, DictionaryId("2")) - Assertions.assertEquals(65, res2.cards.size) - Assertions.assertEquals(0, res2.errors.size) - Assertions.assertEquals(1, res2.dictionaries.size) - Assertions.assertEquals("2", res2.dictionaries.single().dictionaryId.asString()) - - Assertions.assertEquals(LangId("en"), res1.dictionaries.single().sourceLang.langId) - Assertions.assertEquals(LangId("en"), res2.dictionaries.single().sourceLang.langId) + val res2 = repository.findCardsByDictionaryId("2").toList() + assertEquals(65, res2.size) + assertEquals("2", res2.map { it.dictionaryId }.toSet().single()) } @Order(2) @Test fun `test get all cards error unknown dictionary`() { val dictionaryId = "42" - val res = repository.getAllCards(userId, DictionaryId(dictionaryId)) - Assertions.assertEquals(0, res.cards.size) - Assertions.assertEquals(1, res.errors.size) - val error = res.errors[0] - Assertions.assertEquals("database::getAllCards", error.code) - Assertions.assertEquals(dictionaryId, error.field) - Assertions.assertEquals("database", error.group) - Assertions.assertEquals( - """Error while getAllCards: dictionary with id="$dictionaryId" not found""", - error.message - ) - Assertions.assertNull(error.exception) + val res = repository.findCardsByDictionaryId(dictionaryId).toList() + assertEquals(0, res.size) } @Order(4) @Test fun `test create card error unknown dictionary`() { val dictionaryId = "42" - val request = CardEntity( - dictionaryId = DictionaryId(dictionaryId), + val request = DbCard.NULL.copy( + dictionaryId = dictionaryId, words = listOf( - CardWordEntity( + DbCard.Word.NULL.copy( word = "xxx", transcription = "xxx", translations = listOf(listOf("xxx")), @@ -257,160 +229,106 @@ abstract class DbCardRepositoryTest { ), answered = 42, ) - val res = repository.createCard(userId, request) - Assertions.assertEquals(CardEntity.EMPTY, res.card) - val error = assertSingleError(res, dictionaryId, "createCard") - Assertions.assertEquals( - """Error while createCard: dictionary with id="$dictionaryId" not found""", - error.message - ) - Assertions.assertNull(error.exception) - } - - @Order(5) - @Test - fun `test search cards random with unknown`() { - val filter = CardFilter( - dictionaryIds = listOf(DictionaryId("1"), DictionaryId("2"), DictionaryId("3")), - withUnknown = true, - random = true, - length = 300, - ) - val res1 = repository.searchCard(userId, filter) - val res2 = repository.searchCard(userId, filter) - - Assertions.assertEquals(0, res1.errors.size) - Assertions.assertEquals(0, res2.errors.size) - Assertions.assertEquals(300, res1.cards.size) - Assertions.assertEquals(300, res2.cards.size) - Assertions.assertNotEquals(res1, res2) - Assertions.assertEquals(setOf("1", "2"), res1.cards.map { it.dictionaryId }.map { it.asString() }.toSet()) - Assertions.assertEquals(setOf("1", "2"), res2.cards.map { it.dictionaryId }.map { it.asString() }.toSet()) - Assertions.assertEquals( - setOf("1", "2"), - res1.dictionaries.map { it.dictionaryId }.map { it.asString() }.toSet() - ) - Assertions.assertEquals( - setOf("1", "2"), - res2.dictionaries.map { it.dictionaryId }.map { it.asString() }.toSet() - ) + Assertions.assertThrows(DbDataException::class.java) { + repository.createCard(request) + } } @Order(6) @Test fun `test get card & update card success`() { val expected = weatherCardEntity - val prev = repository.getCard(userId, expected.cardId).card - assertCard(expected = expected, actual = prev, ignoreChangeAt = true, ignoreId = false) + val prev = repository.findCardById(expected.cardId) + assertNotNull(prev) + assertCard(expected = expected, actual = prev!!, ignoreChangeAt = true, ignoreId = false) val request = climateCardEntity - val res = repository.updateCard(userId, request) - assertNoErrors(res) - val updated = res.card + val updated = repository.updateCard(request) assertCard(expected = request, actual = updated, ignoreChangeAt = true, ignoreId = false) - val now = repository.getCard(userId, expected.cardId).card - assertCard(expected = request, actual = now, ignoreChangeAt = true, ignoreId = false) + val now = repository.findCardById(expected.cardId) + assertNotNull(now) + assertCard(expected = request, actual = now!!, ignoreChangeAt = true, ignoreId = false) } @Order(7) @Test fun `test update card error unknown card`() { - val id = CardId("4200") - val request = CardEntity.EMPTY.copy( + val id = "4200" + val request = DbCard.NULL.copy( cardId = id, - dictionaryId = DictionaryId("2"), + dictionaryId = "2", words = listOf( - CardWordEntity(word = "XXX", translations = listOf(listOf("xxx"))), + DbCard.Word.NULL.copy(word = "XXX", translations = listOf(listOf("xxx"))), ), ) - val res = repository.updateCard(userId, request) - val error = assertSingleError(res, id.asString(), "updateCard") - Assertions.assertEquals( - """Error while updateCard: card with id="${id.asString()}" not found""", - error.message - ) + Assertions.assertThrows(DbDataException::class.java) { + repository.updateCard(request) + } } @Order(8) @Test fun `test update card error unknown dictionary`() { - val cardId = CardId("42") - val dictionaryId = DictionaryId("4200") - val request = CardEntity.EMPTY.copy( + val cardId = "42" + val dictionaryId = "4200" + val request = DbCard.NULL.copy( cardId = cardId, dictionaryId = dictionaryId, words = listOf( - CardWordEntity( + DbCard.Word.NULL.copy( word = "XXX", translations = listOf(listOf("xxx")), ), ) ) - val res = repository.updateCard(userId, request) - val error = assertSingleError(res, dictionaryId.asString(), "updateCard") - Assertions.assertEquals( - """Error while updateCard: dictionary with id="${dictionaryId.asString()}" not found""", - error.message - ) + Assertions.assertThrows(DbDataException::class.java) { + repository.updateCard(request) + } } - @Order(10) + @Order(11) @Test - fun `test get card & reset card success`() { - val request = snowCardEntity - val prev = repository.getCard(userId, request.cardId).card - assertCard(expected = request, actual = prev, ignoreChangeAt = true, ignoreId = false) + fun `test bulk update & find by card ids - success`() { + val now = systemNow().asKotlin() - val expected = request.copy(answered = 0) - val res = repository.resetCard(userId, request.cardId) - assertNoErrors(res) - val updated = res.card - assertCard(expected = expected, actual = updated, ignoreChangeAt = true, ignoreId = false) + val toUpdate = + sequenceOf(forgiveCardEntity, snowCardEntity, drawCardEntity).map { it.copy(answered = 42) }.toSet() - val now = repository.getCard(userId, request.cardId).card - assertCard(expected = expected, actual = now, ignoreChangeAt = true, ignoreId = false) - } + val updated = repository.updateCards(toUpdate) + assertEquals(3, updated.size) - @Order(11) - @Test - fun `test bulk update success`() { - val now = Clock.System.now() - val res = repository.updateCards( - userId, - setOf(forgiveCardEntity.cardId, snowCardEntity.cardId, drawCardEntity.cardId), - ) { - it.copy(answered = 42) - } - Assertions.assertEquals(0, res.errors.size) - Assertions.assertEquals(3, res.cards.size) - val actual = res.cards.sortedBy { it.cardId.asLong() } - assertCard(expected = drawCardEntity.copy(answered = 42), actual = actual[0], ignoreChangeAt = true) - assertCard(expected = forgiveCardEntity.copy(answered = 42), actual = actual[1], ignoreChangeAt = true) - assertCard(expected = snowCardEntity.copy(answered = 42), actual = actual[2], ignoreChangeAt = true) - actual.forEach { - Assertions.assertTrue(it.changedAt >= now) + val res1 = updated.sortedBy { it.cardId.toLong() } + assertCard(expected = drawCardEntity.copy(answered = 42), actual = res1[0], ignoreChangeAt = true) + assertCard(expected = forgiveCardEntity.copy(answered = 42), actual = res1[1], ignoreChangeAt = true) + assertCard(expected = snowCardEntity.copy(answered = 42), actual = res1[2], ignoreChangeAt = true) + res1.forEach { + assertTrue(it.changedAt >= now) { "expected ${it.changedAt} >= $now" } } + + val res2 = + repository.findCardsByIdIn(setOf(forgiveCardEntity.cardId, snowCardEntity.cardId, drawCardEntity.cardId)) + .sortedBy { it.cardId.toLong() } + .toList() + assertEquals(res1, res2) } @Order(21) @Test fun `test create card success`() { val request = newMurkyCardEntity - val res = repository.createCard(userId, request) - assertNoErrors(res) - assertCard(expected = request, actual = res.card, ignoreChangeAt = true, ignoreId = true) - Assertions.assertTrue(res.card.cardId.asString().matches("\\d+".toRegex())) + val res = repository.createCard(request) + assertCard(expected = request, actual = res, ignoreChangeAt = true, ignoreId = true) + assertTrue(res.cardId.matches("\\d+".toRegex())) } @Order(42) @Test fun `test get card & delete card success`() { - val id = CardId("300") - val res = repository.removeCard(userId, id) - assertNoErrors(res) + val id = "300" + val res = repository.deleteCard(id) + assertEquals(id, res.cardId) - val now = repository.getCard(userId, id).card - Assertions.assertSame(CardEntity.EMPTY, now) + assertNull(repository.findCardById(id)) } } \ No newline at end of file diff --git a/db-common/src/testFixtures/kotlin/DbDictionaryRepositoryTest.kt b/db-common/src/testFixtures/kotlin/DbDictionaryRepositoryTest.kt index 2be7b4a3..8846c3a5 100644 --- a/db-common/src/testFixtures/kotlin/DbDictionaryRepositoryTest.kt +++ b/db-common/src/testFixtures/kotlin/DbDictionaryRepositoryTest.kt @@ -1,20 +1,26 @@ package com.gitlab.sszuev.flashcards.dbcommon -import com.gitlab.sszuev.flashcards.model.common.AppUserId -import com.gitlab.sszuev.flashcards.model.domain.* +import com.gitlab.sszuev.flashcards.repositories.DbDataException +import com.gitlab.sszuev.flashcards.repositories.DbDictionary import com.gitlab.sszuev.flashcards.repositories.DbDictionaryRepository -import org.junit.jupiter.api.* +import com.gitlab.sszuev.flashcards.repositories.DbLang +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder -@Suppress("FunctionName") @TestMethodOrder(MethodOrderer.OrderAnnotation::class) abstract class DbDictionaryRepositoryTest { abstract val repository: DbDictionaryRepository companion object { - private val userId = AppUserId("42") - private val EN = LangEntity( - LangId("en"), listOf( + private const val USER_ID = "c9a414f5-3f75-4494-b664-f4c8b33ff4e6" + + private val EN = DbLang( + langId = "en", + partsOfSpeech = listOf( "noun", "verb", "adjective", @@ -26,9 +32,9 @@ abstract class DbDictionaryRepositoryTest { "article" ) ) - private val RU = LangEntity( - LangId("ru"), - listOf( + private val RU = DbLang( + langId = "ru", + partsOfSpeech = listOf( "существительное", "прилагательное", "числительное", @@ -42,22 +48,35 @@ abstract class DbDictionaryRepositoryTest { "междометие" ) ) + + } + + @Order(1) + @Test + fun `test get dictionary by id`() { + val res1 = repository.findDictionaryById("2") + Assertions.assertNotNull(res1) + Assertions.assertEquals("Weather", res1!!.name) + Assertions.assertEquals(USER_ID, res1.userId) + val res2 = repository.findDictionaryById("1") + Assertions.assertNotNull(res2) + Assertions.assertEquals("Irregular Verbs", res2!!.name) + Assertions.assertEquals(USER_ID, res2.userId) } @Order(1) @Test fun `test get all dictionaries by user-id success`() { - val res = repository.getAllDictionaries(AppUserId("42")) - Assertions.assertTrue(res.errors.isEmpty()) - Assertions.assertEquals(2, res.dictionaries.size) + val res = repository.findDictionariesByUserId(USER_ID).toList() + Assertions.assertEquals(2, res.size) - val businessDictionary = res.dictionaries[0] - Assertions.assertEquals(DictionaryId("1"), businessDictionary.dictionaryId) + val businessDictionary = res[0] + Assertions.assertEquals("1", businessDictionary.dictionaryId) Assertions.assertEquals("Irregular Verbs", businessDictionary.name) Assertions.assertEquals(EN, businessDictionary.sourceLang) Assertions.assertEquals(RU, businessDictionary.targetLang) - val weatherDictionary = res.dictionaries[1] - Assertions.assertEquals(DictionaryId("2"), weatherDictionary.dictionaryId) + val weatherDictionary = res[1] + Assertions.assertEquals("2", weatherDictionary.dictionaryId) Assertions.assertEquals("Weather", weatherDictionary.name) Assertions.assertEquals(EN, weatherDictionary.sourceLang) Assertions.assertEquals(RU, weatherDictionary.targetLang) @@ -66,95 +85,39 @@ abstract class DbDictionaryRepositoryTest { @Order(2) @Test fun `test get all dictionaries by user-id nothing found`() { - val res = repository.getAllDictionaries(AppUserId("42000")) - Assertions.assertEquals(0, res.dictionaries.size) - Assertions.assertTrue(res.errors.isEmpty()) - } - - @Order(3) - @Test - fun `test download dictionary`() { - // Weather - val res = repository.importDictionary(userId, DictionaryId("2")) - Assertions.assertEquals(0, res.errors.size) { "Errors: ${res.errors}" } - val xml = res.resource.data.toString(Charsets.UTF_16) - Assertions.assertTrue(xml.startsWith("""""")) - Assertions.assertEquals(66, xml.split("").size) - Assertions.assertTrue(xml.endsWith("" + System.lineSeparator())) + val res = repository.findDictionariesByUserId("42000").toList() + Assertions.assertEquals(0, res.size) } @Order(4) @Test fun `test delete dictionary success`() { // Business vocabulary (Job) - val res = repository.removeDictionary(userId, DictionaryId("1")) - Assertions.assertTrue(res.errors.isEmpty()) + val res = repository.deleteDictionary("1") + Assertions.assertEquals("1", res.dictionaryId) + Assertions.assertEquals("Irregular Verbs", res.name) + Assertions.assertNull(repository.findDictionaryById("1")) } @Order(4) @Test fun `test delete dictionary not found`() { - val id = DictionaryId("42") - val res = repository.removeDictionary(userId, id) - Assertions.assertEquals(1, res.errors.size) - val error = res.errors[0] - Assertions.assertEquals("database::removeDictionary", error.code) - Assertions.assertEquals(id.asString(), error.field) - Assertions.assertEquals("database", error.group) - Assertions.assertEquals( - """Error while removeDictionary: dictionary with id="${id.asString()}" not found""", - error.message - ) - } - - @Order(5) - @Test - fun `test upload dictionary`() { - val txt = """ - - - - test - - - - - тестировать - - - - - - - """.trimIndent() - val bytes = txt.toByteArray(Charsets.UTF_16) - val res = repository.exportDictionary(AppUserId("42"), ResourceEntity(DictionaryId.NONE, bytes)) - Assertions.assertEquals(0, res.errors.size) { "Errors: ${res.errors}" } - - Assertions.assertEquals("Test Dictionary", res.dictionary.name) - Assertions.assertTrue(res.dictionary.dictionaryId.asString().isNotBlank()) - Assertions.assertEquals(EN, res.dictionary.sourceLang) - Assertions.assertEquals(RU, res.dictionary.targetLang) - Assertions.assertEquals(0, res.dictionary.totalCardsCount) - Assertions.assertEquals(0, res.dictionary.learnedCardsCount) + val id = "42" + Assertions.assertThrows(DbDataException::class.java) { + repository.deleteDictionary(id) + } } @Order(6) @Test fun `test create dictionary success`() { - val given = DictionaryEntity(name = "test-dictionary", sourceLang = RU, targetLang = EN) - val res = repository.createDictionary(AppUserId("42"), given) - Assertions.assertEquals(0, res.errors.size) { "Errors: ${res.errors}" } - Assertions.assertEquals(given.name, res.dictionary.name) - Assertions.assertEquals(RU, res.dictionary.sourceLang) - Assertions.assertEquals(EN, res.dictionary.targetLang) - Assertions.assertNotEquals(DictionaryId.NONE, res.dictionary.dictionaryId) - Assertions.assertTrue(res.dictionary.dictionaryId.asString().matches("\\d+".toRegex())) + val given = + DbDictionary(name = "test-dictionary", sourceLang = RU, targetLang = EN, userId = "42", dictionaryId = "") + val res = repository.createDictionary(given) + Assertions.assertEquals(given.name, res.name) + Assertions.assertEquals(RU, res.sourceLang) + Assertions.assertEquals(EN, res.targetLang) + Assertions.assertFalse(res.dictionaryId.isBlank()) + Assertions.assertTrue(res.dictionaryId.matches("\\d+".toRegex())) } } \ No newline at end of file diff --git a/db-common/src/testFixtures/kotlin/DbUserRepositoryTest.kt b/db-common/src/testFixtures/kotlin/DbUserRepositoryTest.kt deleted file mode 100644 index 7c736b8f..00000000 --- a/db-common/src/testFixtures/kotlin/DbUserRepositoryTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.gitlab.sszuev.flashcards.dbcommon - -import com.gitlab.sszuev.flashcards.model.common.AppAuthId -import com.gitlab.sszuev.flashcards.model.common.AppError -import com.gitlab.sszuev.flashcards.model.common.AppUserEntity -import com.gitlab.sszuev.flashcards.model.common.AppUserId -import com.gitlab.sszuev.flashcards.repositories.DbUserRepository -import com.gitlab.sszuev.flashcards.repositories.UserEntityDbResponse -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test - -abstract class DbUserRepositoryTest { - - abstract val repository: DbUserRepository - - companion object { - val demo = AppUserEntity( - id = AppUserId(42.toString()), - authId = AppAuthId("c9a414f5-3f75-4494-b664-f4c8b33ff4e6"), - ) - - @Suppress("SameParameterValue") - private fun assertAppError(res: UserEntityDbResponse, uuid: String, op: String): AppError { - Assertions.assertEquals(1, res.errors.size) - val error = res.errors[0] - Assertions.assertEquals("database::$op", error.code) - Assertions.assertEquals(uuid, error.field) - Assertions.assertEquals("database", error.group) - return error - } - } - - @Test - fun `test get user error no found`() { - val uuid = "45a34bd8-5472-491e-8e27-84290314ee38" - val res = repository.getUser(AppAuthId(uuid)) - Assertions.assertEquals(AppUserEntity.EMPTY, res.user) - - val error = assertAppError(res, uuid, "getUser") - Assertions.assertEquals( - """Error while getUser: user with uid="$uuid" not found""", - error.message - ) - Assertions.assertNull(error.exception) - } - - @Test - fun `test get user error wrong uuid`() { - val uuid = "xxx" - val res = repository.getUser(AppAuthId(uuid)) - Assertions.assertEquals(AppUserEntity.EMPTY, res.user) - - val error = assertAppError(res, uuid, "getUser") - Assertions.assertEquals( - """Error while getUser: wrong uuid="$uuid"""", - error.message - ) - Assertions.assertNull(error.exception) - } - - @Test - fun `test get user success`() { - val res = repository.getUser(demo.authId) - Assertions.assertNotSame(demo, res.user) - Assertions.assertEquals(demo, res.user) - Assertions.assertEquals(0, res.errors.size) - } -} \ No newline at end of file diff --git a/db-common/src/testFixtures/kotlin/mocks/MockDbCardRepository.kt b/db-common/src/testFixtures/kotlin/mocks/MockDbCardRepository.kt index 33938374..4aa18d01 100644 --- a/db-common/src/testFixtures/kotlin/mocks/MockDbCardRepository.kt +++ b/db-common/src/testFixtures/kotlin/mocks/MockDbCardRepository.kt @@ -1,63 +1,41 @@ package com.gitlab.sszuev.flashcards.dbcommon.mocks -import com.gitlab.sszuev.flashcards.model.common.AppUserId -import com.gitlab.sszuev.flashcards.model.domain.CardEntity -import com.gitlab.sszuev.flashcards.model.domain.CardFilter -import com.gitlab.sszuev.flashcards.model.domain.CardId -import com.gitlab.sszuev.flashcards.model.domain.DictionaryId -import com.gitlab.sszuev.flashcards.repositories.CardDbResponse -import com.gitlab.sszuev.flashcards.repositories.CardsDbResponse +import com.gitlab.sszuev.flashcards.repositories.DbCard import com.gitlab.sszuev.flashcards.repositories.DbCardRepository -import com.gitlab.sszuev.flashcards.repositories.RemoveCardDbResponse /** * Does not work with `io.mockk:mockk` * @see mockk#issue-288 */ class MockDbCardRepository( - private val invokeGetCard: (AppUserId, CardId) -> CardDbResponse = { _, _ -> CardDbResponse.EMPTY }, - private val invokeGetAllCards: (AppUserId, DictionaryId) -> CardsDbResponse = { _, _ -> CardsDbResponse.EMPTY }, - private val invokeSearchCards: (AppUserId, CardFilter) -> CardsDbResponse = { _, _ -> CardsDbResponse.EMPTY }, - private val invokeCreateCard: (AppUserId, CardEntity) -> CardDbResponse = { _, _ -> CardDbResponse.EMPTY }, - private val invokeUpdateCard: (AppUserId, CardEntity) -> CardDbResponse = { _, _ -> CardDbResponse.EMPTY }, - private val invokeUpdateCards: (AppUserId, Iterable, (CardEntity) -> CardEntity) -> CardsDbResponse = { _, _, _ -> CardsDbResponse.EMPTY }, - private val invokeResetCard: (AppUserId, CardId) -> CardDbResponse = { _, _ -> CardDbResponse.EMPTY }, - private val invokeDeleteCard: (AppUserId, CardId) -> RemoveCardDbResponse = { _, _ -> RemoveCardDbResponse.EMPTY }, + private val invokeFindCardById: (String) -> DbCard? = { null }, + private val invokeFindCardsByDictionaryId: (String) -> Sequence = { emptySequence() }, + private val invokeFindCardsByDictionaryIdIn: (Iterable) -> Sequence = { emptySequence() }, + private val invokeFindCardsByIdIn: (Iterable) -> Sequence = { emptySequence() }, + private val invokeCreateCard: (DbCard) -> DbCard = { DbCard.NULL }, + private val invokeCreateCards: (Iterable) -> List = { emptyList() }, + private val invokeUpdateCard: (DbCard) -> DbCard = { DbCard.NULL }, + private val invokeUpdateCards: (Iterable) -> List = { emptyList() }, + private val invokeDeleteCard: (String) -> DbCard = { _ -> DbCard.NULL }, ) : DbCardRepository { - override fun getCard(userId: AppUserId, cardId: CardId): CardDbResponse { - return invokeGetCard(userId, cardId) - } - - override fun getAllCards(userId: AppUserId, dictionaryId: DictionaryId): CardsDbResponse { - return invokeGetAllCards(userId, dictionaryId) - } - - override fun searchCard(userId: AppUserId, filter: CardFilter): CardsDbResponse { - return invokeSearchCards(userId, filter) - } - - override fun createCard(userId: AppUserId, cardEntity: CardEntity): CardDbResponse { - return invokeCreateCard(userId, cardEntity) - } - - override fun updateCard(userId: AppUserId, cardEntity: CardEntity): CardDbResponse { - return invokeUpdateCard(userId, cardEntity) - } - - override fun updateCards( - userId: AppUserId, - cardIds: Iterable, - update: (CardEntity) -> CardEntity - ): CardsDbResponse { - return invokeUpdateCards(userId, cardIds, update) - } - - override fun resetCard(userId: AppUserId, cardId: CardId): CardDbResponse { - return invokeResetCard(userId, cardId) - } - - override fun removeCard(userId: AppUserId, cardId: CardId): RemoveCardDbResponse { - return invokeDeleteCard(userId, cardId) - } + override fun findCardById(cardId: String): DbCard? = invokeFindCardById(cardId) + + override fun findCardsByDictionaryId(dictionaryId: String): Sequence = + invokeFindCardsByDictionaryId(dictionaryId) + + override fun findCardsByDictionaryIdIn(dictionaryIds: Iterable): Sequence = + invokeFindCardsByDictionaryIdIn(dictionaryIds) + + override fun findCardsByIdIn(cardIds: Iterable): Sequence = invokeFindCardsByIdIn(cardIds) + + override fun createCard(cardEntity: DbCard): DbCard = invokeCreateCard(cardEntity) + + override fun createCards(cardEntities: Iterable): List = invokeCreateCards(cardEntities) + + override fun updateCard(cardEntity: DbCard): DbCard = invokeUpdateCard(cardEntity) + + override fun updateCards(cardEntities: Iterable): List = invokeUpdateCards(cardEntities) + + override fun deleteCard(cardId: String): DbCard = invokeDeleteCard(cardId) } \ No newline at end of file diff --git a/db-common/src/testFixtures/kotlin/mocks/MockDbDictionaryRepository.kt b/db-common/src/testFixtures/kotlin/mocks/MockDbDictionaryRepository.kt index 84dd3330..32e2a5b0 100644 --- a/db-common/src/testFixtures/kotlin/mocks/MockDbDictionaryRepository.kt +++ b/db-common/src/testFixtures/kotlin/mocks/MockDbDictionaryRepository.kt @@ -1,40 +1,25 @@ package com.gitlab.sszuev.flashcards.dbcommon.mocks -import com.gitlab.sszuev.flashcards.model.common.AppUserId -import com.gitlab.sszuev.flashcards.model.domain.DictionaryEntity -import com.gitlab.sszuev.flashcards.model.domain.DictionaryId -import com.gitlab.sszuev.flashcards.model.domain.ResourceEntity +import com.gitlab.sszuev.flashcards.repositories.DbDictionary import com.gitlab.sszuev.flashcards.repositories.DbDictionaryRepository -import com.gitlab.sszuev.flashcards.repositories.DictionariesDbResponse -import com.gitlab.sszuev.flashcards.repositories.DictionaryDbResponse -import com.gitlab.sszuev.flashcards.repositories.ImportDictionaryDbResponse -import com.gitlab.sszuev.flashcards.repositories.RemoveDictionaryDbResponse class MockDbDictionaryRepository( - private val invokeGetAllDictionaries: (AppUserId) -> DictionariesDbResponse = { DictionariesDbResponse.EMPTY }, - private val invokeCreateDictionary: (AppUserId, DictionaryEntity) -> DictionaryDbResponse = { _, _ -> DictionaryDbResponse.EMPTY }, - private val invokeDeleteDictionary: (AppUserId, DictionaryId) -> RemoveDictionaryDbResponse = { _, _ -> RemoveDictionaryDbResponse.EMPTY }, - private val invokeDownloadDictionary: (AppUserId, DictionaryId) -> ImportDictionaryDbResponse = { _, _ -> ImportDictionaryDbResponse.EMPTY }, - private val invokeUploadDictionary: (AppUserId, ResourceEntity) -> DictionaryDbResponse = { _, _ -> DictionaryDbResponse.EMPTY }, + private val invokeFindDictionaryById: (String) -> DbDictionary? = { null }, + private val invokeFindDictionariesByIdIn: (Iterable) -> Sequence = { emptySequence() }, + private val invokeGetAllDictionaries: (String) -> Sequence = { emptySequence() }, + private val invokeCreateDictionary: (DbDictionary) -> DbDictionary = { DbDictionary.NULL }, + private val invokeDeleteDictionary: (String) -> DbDictionary = { DbDictionary.NULL }, ) : DbDictionaryRepository { - override fun getAllDictionaries(userId: AppUserId): DictionariesDbResponse { - return invokeGetAllDictionaries(userId) - } + override fun findDictionaryById(dictionaryId: String): DbDictionary? = invokeFindDictionaryById(dictionaryId) - override fun createDictionary(userId: AppUserId, entity: DictionaryEntity): DictionaryDbResponse { - return invokeCreateDictionary(userId, entity) - } + override fun findDictionariesByIdIn(dictionaryIds: Iterable): Sequence = + invokeFindDictionariesByIdIn(dictionaryIds) - override fun removeDictionary(userId: AppUserId, dictionaryId: DictionaryId): RemoveDictionaryDbResponse { - return invokeDeleteDictionary(userId, dictionaryId) - } + override fun findDictionariesByUserId(userId: String): Sequence = invokeGetAllDictionaries(userId) - override fun importDictionary(userId: AppUserId, dictionaryId: DictionaryId): ImportDictionaryDbResponse { - return invokeDownloadDictionary(userId, dictionaryId) - } + override fun createDictionary(entity: DbDictionary): DbDictionary = invokeCreateDictionary(entity) + + override fun deleteDictionary(dictionaryId: String): DbDictionary = invokeDeleteDictionary(dictionaryId) - override fun exportDictionary(userId: AppUserId, resource: ResourceEntity): DictionaryDbResponse { - return invokeUploadDictionary(userId, resource) - } } \ No newline at end of file diff --git a/db-common/src/testFixtures/kotlin/mocks/MockDbUserRepository.kt b/db-common/src/testFixtures/kotlin/mocks/MockDbUserRepository.kt deleted file mode 100644 index 6b17db86..00000000 --- a/db-common/src/testFixtures/kotlin/mocks/MockDbUserRepository.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.gitlab.sszuev.flashcards.dbcommon.mocks - -import com.gitlab.sszuev.flashcards.model.common.AppAuthId -import com.gitlab.sszuev.flashcards.repositories.DbUserRepository -import com.gitlab.sszuev.flashcards.repositories.UserEntityDbResponse - -class MockDbUserRepository( - private val invokeGetUser: (AppAuthId) -> UserEntityDbResponse = { UserEntityDbResponse.EMPTY } -) : DbUserRepository { - - override fun getUser(authId: AppAuthId): UserEntityDbResponse { - return invokeGetUser(authId) - } -} \ No newline at end of file diff --git a/db-common/src/main/kotlin/documents/IdGenerator.kt b/db-mem/src/main/kotlin/IdGenerator.kt similarity index 61% rename from db-common/src/main/kotlin/documents/IdGenerator.kt rename to db-mem/src/main/kotlin/IdGenerator.kt index 8b3e4221..af741265 100644 --- a/db-common/src/main/kotlin/documents/IdGenerator.kt +++ b/db-mem/src/main/kotlin/IdGenerator.kt @@ -1,4 +1,4 @@ -package com.gitlab.sszuev.flashcards.common.documents +package com.gitlab.sszuev.flashcards.dbmem interface IdGenerator { fun nextDictionaryId(): Long diff --git a/db-mem/src/main/kotlin/IdSequences.kt b/db-mem/src/main/kotlin/IdSequences.kt index 6c9d1e67..8a2fa6d4 100644 --- a/db-mem/src/main/kotlin/IdSequences.kt +++ b/db-mem/src/main/kotlin/IdSequences.kt @@ -1,20 +1,13 @@ package com.gitlab.sszuev.flashcards.dbmem -import com.gitlab.sszuev.flashcards.common.documents.IdGenerator import java.util.concurrent.atomic.AtomicLong internal class IdSequences( - initUserId: Long = 0, initDictionaryId: Long = 0, initCardId: Long = 0, ) : IdGenerator { private val dictionarySequence = AtomicLong(initDictionaryId) private val cardSequence = AtomicLong(initCardId) - private val userSequence = AtomicLong(initUserId) - - fun nextUserId(): Long { - return userSequence.incrementAndGet() - } override fun nextDictionaryId(): Long { return dictionarySequence.incrementAndGet() diff --git a/db-mem/src/main/kotlin/MemDatabase.kt b/db-mem/src/main/kotlin/MemDatabase.kt index d09e1541..9165cabe 100644 --- a/db-mem/src/main/kotlin/MemDatabase.kt +++ b/db-mem/src/main/kotlin/MemDatabase.kt @@ -1,9 +1,8 @@ package com.gitlab.sszuev.flashcards.dbmem -import com.gitlab.sszuev.flashcards.common.systemNow import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbCard import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbDictionary -import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbUser +import com.gitlab.sszuev.flashcards.systemNow import org.apache.commons.csv.CSVFormat import org.apache.commons.csv.CSVParser import org.apache.commons.csv.CSVPrinter @@ -15,19 +14,18 @@ import java.nio.file.Paths import java.time.LocalDateTime import java.time.OffsetDateTime import java.time.ZoneOffset -import java.util.UUID import java.util.concurrent.ConcurrentHashMap import kotlin.concurrent.timer import kotlin.io.path.inputStream import kotlin.io.path.outputStream /** - * A dictionary store, attached to file system or classpath. - * In the first case it is persistent. + * A dictionary store, attached to a file system or classpath. + * In the first case, it is persistent. */ class MemDatabase private constructor( private val idGenerator: IdSequences, - private val resources: MutableMap, + private val resources: MutableMap>, private val databaseHomeDirectory: String?, ) { @@ -37,36 +35,24 @@ class MemDatabase private constructor( @Volatile private var cardsChanged = false - @Volatile - private var usersChanged = false - fun countUsers(): Long { return resources.size.toLong() } - fun findUsers(): Sequence { - return resources.asSequence().map { it.value.user } - } - - fun findUserByUuid(userUuid: UUID): MemDbUser? { - return resources.asSequence().map { it.value.user }.singleOrNull { it.uuid == userUuid } + fun findUserIds(): Sequence { + return resources.keys.asSequence() } - fun saveUser(user: MemDbUser): MemDbUser { - require(user.id != null || user.changedAt == null) - val id = user.id ?: idGenerator.nextUserId() - val res = user.copy(id = id, changedAt = systemNow()) - resources[id] = UserResource(res) - usersChanged = true - return res + fun containsUser(id: String): Boolean { + return resources.contains(id) } fun countDictionaries(): Long { - return resources.asSequence().map { it.value.dictionaries.size.toLong() }.sum() + return resources.asSequence().sumOf { it.value.size.toLong() } } - fun findDictionariesByUserId(userId: Long): Sequence { - return resources[userId]?.dictionaries?.asSequence()?.map { it.value.dictionary } ?: emptySequence() + fun findDictionariesByUserId(userId: String): Sequence { + return resources[userId]?.values?.asSequence()?.map { it.dictionary } ?: emptySequence() } fun findDictionariesByIds(dictionaryIds: Collection): Sequence { @@ -79,17 +65,20 @@ class MemDatabase private constructor( } fun saveDictionary(dictionary: MemDbDictionary): MemDbDictionary { - val resource = - requireNotNull(resources[dictionary.userId]) { "Unknown user ${dictionary.userId}" } + val userId = requireNotNull(dictionary.userId) { "User id is required" } + val resource = resources.computeIfAbsent(userId) { ConcurrentHashMap() } val id = dictionary.id ?: idGenerator.nextDictionaryId() - val res = dictionary.copy(id = id, changedAt = dictionary.changedAt ?: OffsetDateTime.now(ZoneOffset.UTC).toLocalDateTime()) - resource.dictionaries[id] = DictionaryResource(res) + val res = dictionary.copy( + id = id, + changedAt = dictionary.changedAt ?: OffsetDateTime.now(ZoneOffset.UTC).toLocalDateTime() + ) + resource[id] = DictionaryResource(res) dictionariesChanged = true return res } fun deleteDictionaryById(dictionaryId: Long): Boolean { - val resource = resources.map { it.value.dictionaries }.singleOrNull { it[dictionaryId] != null } + val resource = resources.map { it.value }.singleOrNull { it[dictionaryId] != null } return if (resource?.remove(dictionaryId) != null) { dictionariesChanged = true true @@ -145,65 +134,48 @@ class MemDatabase private constructor( } private fun dictionaryResourceById(dictionaryId: Long): DictionaryResource? { - return resources.values.mapNotNull { it.dictionaries[dictionaryId] }.singleOrNull() + return resources.values.mapNotNull { it[dictionaryId] }.singleOrNull() } private fun dictionaryResources(): Sequence { - return resources.values.asSequence().flatMap { it.dictionaries.values.asSequence() } + return resources.values.asSequence().flatMap { it.values.asSequence() } } private fun cards(): Sequence { - return resources.values.asSequence().flatMap { it.dictionaries.values.asSequence() } + return resources.values.asSequence().flatMap { it.values.asSequence() } .flatMap { it.cards.values.asSequence() } } - private fun users(): Sequence { - return resources.values.asSequence().map { it.user } - } - private fun saveData() { if (databaseHomeDirectory == null) { return } - if (usersChanged) { - val users = users().sortedBy { it.id }.toList() - Paths.get(databaseHomeDirectory).resolve(usersDbFile).outputStream().use { - writeUsers(users, it) - } - usersChanged = false - } if (cardsChanged) { val cards = cards().sortedBy { it.id }.toList() - Paths.get(databaseHomeDirectory).resolve(cardsDbFile).outputStream().use { + Paths.get(databaseHomeDirectory).resolve(CARDS_DB_FILE).outputStream().use { writeCards(cards, it) } cardsChanged = false } if (dictionariesChanged) { val dictionaries = dictionaryResources().map { it.dictionary }.sortedBy { it.id }.toList() - Paths.get(databaseHomeDirectory).resolve(dictionariesDbFile).outputStream().use { + Paths.get(databaseHomeDirectory).resolve(DICTIONARY_DB_FILE).outputStream().use { writeDictionaries(dictionaries, it) } dictionariesChanged = false } } - private data class UserResource( - val user: MemDbUser, - val dictionaries: MutableMap = ConcurrentHashMap(), - ) - private data class DictionaryResource( val dictionary: MemDbDictionary, val cards: MutableMap = ConcurrentHashMap(), ) companion object { - private const val usersDbFile = "users.csv" - private const val dictionariesDbFile = "dictionaries.csv" - private const val cardsDbFile = "cards.csv" + private const val DICTIONARY_DB_FILE = "dictionaries.csv" + private const val CARDS_DB_FILE = "cards.csv" + private const val CLASSPATH_PREFIX = "classpath:" - private const val classpathPrefix = "classpath:" private val logger = LoggerFactory.getLogger(MemDatabase::class.java) /** @@ -240,20 +212,18 @@ class MemDatabase private constructor( * Loads dictionary store from classpath or directory. */ internal fun load(databaseLocation: String): MemDatabase { - val fromClassPath = databaseLocation.startsWith(classpathPrefix) + val fromClassPath = databaseLocation.startsWith(CLASSPATH_PREFIX) val res = if (fromClassPath) { loadDatabaseResourcesFromClassPath(databaseLocation) } else { loadDatabaseResourcesFromDirectory(databaseLocation) } - val maxUserId = res.keys.max() - val maxDictionaryId = res.values.asSequence().flatMap { it.dictionaries.keys.asSequence() }.max() + val maxDictionaryId = res.values.asSequence().flatMap { it.keys }.max() val maxCardId = res.values.asSequence() - .flatMap { it.dictionaries.asSequence() } - .flatMap { it.value.cards.keys.asSequence() } + .flatMap { it.values.asSequence() } + .flatMap { it.cards.map { card -> card.key } } .max() val ids = IdSequences( - initUserId = maxUserId, initDictionaryId = maxDictionaryId, initCardId = maxCardId, ) @@ -266,35 +236,25 @@ class MemDatabase private constructor( private fun loadDatabaseResourcesFromDirectory( directoryDbLocation: String, - ): MutableMap { - val usersFile = Paths.get(directoryDbLocation).resolve(usersDbFile).toRealPath() - val cardsFile = Paths.get(directoryDbLocation).resolve(cardsDbFile).toRealPath() - val dictionariesFile = Paths.get(directoryDbLocation).resolve(dictionariesDbFile).toRealPath() - logger.info("Load users data from file: <$usersFile>.") - val users = usersFile.inputStream().use { - readUsers(it) - } - logger.info("Load cards data from file: <$cardsFile>.") - val cards = cardsFile.inputStream().use { + ): MutableMap> { + val cardFile = Paths.get(directoryDbLocation).resolve(CARDS_DB_FILE).toRealPath() + val dictionaryFile = Paths.get(directoryDbLocation).resolve(DICTIONARY_DB_FILE).toRealPath() + logger.info("Load cards data from file: <$cardFile>.") + val cards = cardFile.inputStream().use { readCards(it) } - logger.info("Load dictionaries data from file: <$dictionariesFile>.") - val dictionaries = dictionariesFile.inputStream().use { + logger.info("Load dictionaries data from file: <$dictionaryFile>.") + val dictionaries = dictionaryFile.inputStream().use { readDictionaries(it) } - return composeDatabaseData(directoryDbLocation, users, dictionaries, cards) + return composeDatabaseData(directoryDbLocation, dictionaries, cards) } private fun loadDatabaseResourcesFromClassPath( classpathDbLocation: String, - ): MutableMap { - val usersFile = resolveClasspathResource(classpathDbLocation, usersDbFile) - val cardsFile = resolveClasspathResource(classpathDbLocation, cardsDbFile) - val dictionariesFile = resolveClasspathResource(classpathDbLocation, dictionariesDbFile) - logger.info("Load users data from classpath: <$usersFile>.") - val users = checkNotNull(MemDatabase::class.java.getResourceAsStream(usersFile)).use { - readUsers(it) - } + ): MutableMap> { + val cardsFile = resolveClasspathResource(classpathDbLocation, CARDS_DB_FILE) + val dictionariesFile = resolveClasspathResource(classpathDbLocation, DICTIONARY_DB_FILE) logger.info("Load cards data from classpath: <$cardsFile>.") val cards = checkNotNull(MemDatabase::class.java.getResourceAsStream(cardsFile)).use { readCards(it) @@ -303,39 +263,33 @@ class MemDatabase private constructor( val dictionaries = checkNotNull(MemDatabase::class.java.getResourceAsStream(dictionariesFile)).use { readDictionaries(it) } - return composeDatabaseData(classpathDbLocation, users, dictionaries, cards) + return composeDatabaseData(classpathDbLocation, dictionaries, cards) } private fun composeDatabaseData( dbLocation: String, - users: List, dictionaries: List, cards: List - ): MutableMap { - val res = users.map { user -> - val userDictionaries = dictionaries.asSequence() - .filter { it.userId == user.id } - .map { dictionary -> + ): MutableMap> { + val dictionaryIds = mutableSetOf() + val res = dictionaries + .filter { it.userId != null } + .groupBy { checkNotNull(it.userId) } + .mapValues { (_, userDictionaries) -> + userDictionaries.map { dictionary -> + dictionaryIds.add(checkNotNull(dictionary.id)) val dictionaryCards = cards.asSequence() .filter { it.dictionaryId == dictionary.id } .associateByTo(ConcurrentHashMap()) { checkNotNull(it.id) } DictionaryResource(dictionary, dictionaryCards) - } - .associateByTo(ConcurrentHashMap()) { checkNotNull(it.dictionary.id) } - UserResource(user, userDictionaries) - }.associateByTo(ConcurrentHashMap()) { checkNotNull(it.user.id) } - - val unattachedDictionaryIds = dictionaries.asSequence().map { it.id }.toMutableSet() - val unattachedCardIds = cards.asSequence().map { it.id }.toMutableSet() - val dictionariesCount = res.values.asSequence() - .flatMap { it.dictionaries.keys.asSequence() } - .onEach { unattachedDictionaryIds.remove(it) } - .count() - val cardsCount = res.values.asSequence() - .flatMap { it.dictionaries.values.asSequence() } - .flatMap { it.cards.keys.asSequence() } - .onEach { unattachedCardIds.remove(it) } - .count() + }.associateByTo(ConcurrentHashMap()) { checkNotNull(it.dictionary.id) } + }.toMap(ConcurrentHashMap()) + + val unattachedDictionaryIds = dictionaries.asSequence().filter { it.userId == null }.map { it.id }.toList() + val unattachedCardIds = + cards.asSequence().filterNot { dictionaryIds.contains(it.dictionaryId) }.map { it.id }.toMutableSet() + val dictionariesCount = res.values.sumOf { it.size } + val cardsCount = res.values.flatMap { it.values }.sumOf { it.cards.size } logger.info("In the store=<$dbLocation> there are ${res.size} users, $dictionariesCount dictionaries and $cardsCount cards.") if (unattachedDictionaryIds.isNotEmpty()) { @@ -344,18 +298,8 @@ class MemDatabase private constructor( if (unattachedCardIds.isNotEmpty()) { logger.warn("The ${unattachedCardIds.size} cards assigned to unknown dictionaries. ids = $unattachedCardIds") } - return res - } - - private fun readUsers(inputStream: InputStream): List = userCsvFormat(false).read(inputStream).use { - it.records.map { record -> - MemDbUser( - id = record.value("id").toLong(), - uuid = UUID.fromString(record.value("uuid")), - details = fromJsonStringToMemDbUserDetails(record.value("details")), - changedAt = LocalDateTime.parse(record.value("changed_at")), - ) - } + @Suppress("UNCHECKED_CAST") + return res as MutableMap> } private fun readDictionaries(inputStream: InputStream): List = @@ -364,7 +308,7 @@ class MemDatabase private constructor( MemDbDictionary( id = record.value("id").toLong(), name = record.value("name"), - userId = record.value("user_id").toLong(), + userId = record.value("user_id"), sourceLanguage = createMemDbLanguage(record.get("source_lang")), targetLanguage = createMemDbLanguage(record.get("target_lang")), details = fromJsonStringToMemDbDictionaryDetails(record.value("details")), @@ -386,18 +330,6 @@ class MemDatabase private constructor( } } - private fun writeUsers(users: Collection, outputStream: OutputStream) = - userCsvFormat(true).write(outputStream).use { - users.forEach { user -> - it.printRecord( - user.id, - user.uuid, - user.detailsAsJsonString(), - user.changedAt, - ) - } - } - private fun writeDictionaries(dictionaries: Collection, outputStream: OutputStream) = dictionaryCsvFormat(true).write(outputStream).use { dictionaries.forEach { dictionary -> @@ -427,70 +359,46 @@ class MemDatabase private constructor( } } - private fun userCsvFormat(withHeader: Boolean): CSVFormat { - return CSVFormat.DEFAULT.builder() - .setHeader( - "id", - "uuid", - "details", - "changed_at", - ) - .setSkipHeaderRecord(!withHeader) - .build() - } - - private fun dictionaryCsvFormat(withHeader: Boolean): CSVFormat { - return CSVFormat.DEFAULT.builder() - .setHeader( - "id", - "name", - "user_id", - "source_lang", - "target_lang", - "details", - "changed_at", - ) - .setSkipHeaderRecord(!withHeader) - .build() - } - - private fun cardCsvFormat(withHeader: Boolean): CSVFormat { - return CSVFormat.DEFAULT.builder() - .setHeader( - "id", - "dictionary_id", - "words", - "details", - "answered", - "changed_at", - ) - .setSkipHeaderRecord(!withHeader) - .build() - } + private fun dictionaryCsvFormat(withHeader: Boolean): CSVFormat = CSVFormat.DEFAULT.builder() + .setHeader( + "id", + "name", + "user_id", + "source_lang", + "target_lang", + "details", + "changed_at", + ) + .setSkipHeaderRecord(!withHeader) + .build() + + private fun cardCsvFormat(withHeader: Boolean): CSVFormat = CSVFormat.DEFAULT.builder() + .setHeader( + "id", + "dictionary_id", + "words", + "details", + "answered", + "changed_at", + ) + .setSkipHeaderRecord(!withHeader) + .build() - private fun CSVRecord.value(key: String): String { - return requireNotNull(get(key)) { "null value for '$key'. record = $this" } - } + private fun CSVRecord.value(key: String): String = + requireNotNull(get(key)) { "null value for '$key'. record = $this" } - private fun CSVRecord.valueOrNull(key: String): String? { - return get(key)?.takeIf { it.isNotBlank() } - } + private fun CSVRecord.valueOrNull(key: String): String? = get(key)?.takeIf { it.isNotBlank() } - private fun CSVFormat.write(outputStream: OutputStream): CSVPrinter { - return print(outputStream.bufferedWriter(charset = Charsets.UTF_8)) - } + private fun CSVFormat.write(outputStream: OutputStream): CSVPrinter = + print(outputStream.bufferedWriter(charset = Charsets.UTF_8)) - private fun CSVFormat.read(inputStream: InputStream): CSVParser { - return parse(inputStream.bufferedReader(charset = Charsets.UTF_8)) - } + private fun CSVFormat.read(inputStream: InputStream): CSVParser = + parse(inputStream.bufferedReader(charset = Charsets.UTF_8)) - private fun Collection.asSet(): Set { - return if (this is Set) this else toSet() - } + private fun Collection.asSet(): Set = if (this is Set) this else toSet() - private fun resolveClasspathResource(classpathDir: String, classpathFilename: String): String { - return "${classpathDir.substringAfter(classpathPrefix)}/$classpathFilename".replace("//", "/") - } + private fun resolveClasspathResource(classpathDir: String, classpathFilename: String): String = + "${classpathDir.substringAfter(CLASSPATH_PREFIX)}/$classpathFilename".replace("//", "/") } } \ No newline at end of file diff --git a/db-mem/src/main/kotlin/MemDbCardRepository.kt b/db-mem/src/main/kotlin/MemDbCardRepository.kt index 3dccc0bd..40aa14bb 100644 --- a/db-mem/src/main/kotlin/MemDbCardRepository.kt +++ b/db-mem/src/main/kotlin/MemDbCardRepository.kt @@ -1,207 +1,60 @@ package com.gitlab.sszuev.flashcards.dbmem -import com.gitlab.sszuev.flashcards.common.SysConfig -import com.gitlab.sszuev.flashcards.common.asLong -import com.gitlab.sszuev.flashcards.common.dbError -import com.gitlab.sszuev.flashcards.common.documents.DocumentCardStatus -import com.gitlab.sszuev.flashcards.common.forbiddenEntityDbError -import com.gitlab.sszuev.flashcards.common.noCardFoundDbError -import com.gitlab.sszuev.flashcards.common.noDictionaryFoundDbError -import com.gitlab.sszuev.flashcards.common.status -import com.gitlab.sszuev.flashcards.common.systemNow import com.gitlab.sszuev.flashcards.common.validateCardEntityForCreate import com.gitlab.sszuev.flashcards.common.validateCardEntityForUpdate -import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbDictionary -import com.gitlab.sszuev.flashcards.model.Id -import com.gitlab.sszuev.flashcards.model.common.AppError -import com.gitlab.sszuev.flashcards.model.common.AppUserId -import com.gitlab.sszuev.flashcards.model.domain.CardEntity -import com.gitlab.sszuev.flashcards.model.domain.CardFilter -import com.gitlab.sszuev.flashcards.model.domain.CardId -import com.gitlab.sszuev.flashcards.model.domain.DictionaryId -import com.gitlab.sszuev.flashcards.repositories.CardDbResponse -import com.gitlab.sszuev.flashcards.repositories.CardsDbResponse +import com.gitlab.sszuev.flashcards.repositories.DbCard import com.gitlab.sszuev.flashcards.repositories.DbCardRepository -import com.gitlab.sszuev.flashcards.repositories.RemoveCardDbResponse -import kotlin.random.Random +import com.gitlab.sszuev.flashcards.repositories.DbDataException +import com.gitlab.sszuev.flashcards.systemNow class MemDbCardRepository( dbConfig: MemDbConfig = MemDbConfig(), - private val sysConfig: SysConfig = SysConfig(), ) : DbCardRepository { private val database = MemDatabase.get(dbConfig.dataLocation) - override fun getCard(userId: AppUserId, cardId: CardId): CardDbResponse { - val card = - database.findCardById(cardId.asLong()) ?: return CardDbResponse(noCardFoundDbError("getCard", cardId)) - val errors = mutableListOf() - checkDictionaryUser("getCard", userId, card.dictionaryId.asDictionaryId(), cardId, errors) - if (errors.isNotEmpty()) { - return CardDbResponse(errors = errors) - } - return CardDbResponse(card = card.toCardEntity()) - } + override fun findCardById(cardId: String): DbCard? = + database.findCardById(require(cardId.isNotBlank()).run { cardId.toLong() })?.toDbCard() - override fun getAllCards(userId: AppUserId, dictionaryId: DictionaryId): CardsDbResponse { - val id = dictionaryId.asLong() - val errors = mutableListOf() - val dictionary = checkDictionaryUser("getAllCards", userId, dictionaryId, dictionaryId, errors) - if (errors.isNotEmpty() || dictionary == null) { - return CardsDbResponse(errors = errors) - } - val cards = database.findCardsByDictionaryId(id).map { it.toCardEntity() }.toList() - val dictionaries = listOf(dictionary.toDictionaryEntity()) - return CardsDbResponse( - cards = cards, - dictionaries = dictionaries, - errors = emptyList() - ) - } + override fun findCardsByDictionaryId(dictionaryId: String): Sequence = + database.findCardsByDictionaryId(require(dictionaryId.isNotBlank()).run { dictionaryId.toLong() }) + .map { it.toDbCard() } - override fun searchCard(userId: AppUserId, filter: CardFilter): CardsDbResponse { - require(filter.length != 0) { "zero length is specified" } - val ids = filter.dictionaryIds.map { it.asLong() } - val dictionariesFromDb = database.findDictionariesByIds(ids).sortedBy { it.id }.toSet() - if (dictionariesFromDb.isEmpty()) { - return CardsDbResponse() - } - val forbiddenIds = - dictionariesFromDb.filter { it.userId != userId.asLong() }.map { checkNotNull(it.id) }.toSet() - val errors = forbiddenIds.map { forbiddenEntityDbError("searchCards", it.asDictionaryId(), userId) } - if (errors.isNotEmpty()) { - return CardsDbResponse(cards = emptyList(), dictionaries = emptyList(), errors = errors) - } - val dictionaries = dictionariesFromDb.filterNot { it.id in forbiddenIds }.map { it.toDictionaryEntity() } - var cardsFromDb = database.findCardsByDictionaryIds(ids) - if (!filter.withUnknown) { - cardsFromDb = cardsFromDb.filter { sysConfig.status(it.answered) != DocumentCardStatus.LEARNED } - } - if (filter.random) { - cardsFromDb = cardsFromDb.shuffled(Random.Default) - } - val cards = - (if (filter.length < 0) cardsFromDb else cardsFromDb.take(filter.length)).map { it.toCardEntity() }.toList() - return CardsDbResponse(cards = cards, dictionaries = dictionaries) - } + override fun findCardsByDictionaryIdIn(dictionaryIds: Iterable): Sequence = + database.findCardsByDictionaryIds(dictionaryIds.onEach { require(it.isNotBlank()) }.map { it.toLong() }) + .map { it.toDbCard() } - override fun createCard(userId: AppUserId, cardEntity: CardEntity): CardDbResponse { - validateCardEntityForCreate(cardEntity) - val errors = mutableListOf() - checkDictionaryUser("createCard", userId, cardEntity.dictionaryId, cardEntity.dictionaryId, errors) - if (errors.isNotEmpty()) { - return CardDbResponse(errors = errors) - } - val timestamp = systemNow() - return CardDbResponse( - card = database.saveCard(cardEntity.toMemDbCard().copy(changedAt = timestamp)).toCardEntity() - ) - } + override fun findCardsByIdIn(cardIds: Iterable): Sequence = + database.findCardsById(cardIds.onEach { require(it.isNotBlank()) }.map { it.toLong() }) + .map { it.toDbCard() } - override fun updateCard(userId: AppUserId, cardEntity: CardEntity): CardDbResponse { - validateCardEntityForUpdate(cardEntity) + override fun createCard(cardEntity: DbCard): DbCard { + validateCardEntityForCreate(cardEntity) val timestamp = systemNow() - val found = database.findCardById(cardEntity.cardId.asLong()) ?: return CardDbResponse( - noCardFoundDbError("updateCard", cardEntity.cardId) - ) - val errors = mutableListOf() - val foundDictionary = - checkDictionaryUser("updateCard", userId, cardEntity.dictionaryId, cardEntity.cardId, errors) - if (foundDictionary != null && foundDictionary.id != cardEntity.dictionaryId.asLong()) { - errors.add( - dbError( - operation = "updateCard", - fieldName = cardEntity.cardId.asString(), - details = "given and found dictionary ids do not match: ${cardEntity.dictionaryId.asString()} != ${found.dictionaryId}" - ) - ) + return try { + database.saveCard(cardEntity.toMemDbCard().copy(changedAt = timestamp)).toDbCard() + } catch (ex: Exception) { + throw DbDataException("Can't create card $cardEntity", ex) } - if (errors.isNotEmpty()) { - return CardDbResponse(errors = errors) - } - return CardDbResponse( - card = database.saveCard(cardEntity.toMemDbCard().copy(changedAt = timestamp)).toCardEntity() - ) } - override fun updateCards( - userId: AppUserId, - cardIds: Iterable, - update: (CardEntity) -> CardEntity - ): CardsDbResponse { - val timestamp = systemNow() - val ids = cardIds.map { it.asLong() } - val dbCards = database.findCardsById(ids).associateBy { checkNotNull(it.id) } - val errors = mutableListOf() - val dbDictionaries = mutableMapOf() - dbCards.forEach { - val dictionary = dbDictionaries.computeIfAbsent(checkNotNull(it.value.dictionaryId)) { k -> - checkNotNull(database.findDictionaryById(k)) - } - if (dictionary.userId != userId.asLong()) { - errors.add(forbiddenEntityDbError("updateCards", it.key.asCardId(), userId)) - } - } - if (errors.isNotEmpty()) { - return CardsDbResponse(errors = errors) - } - val cards = dbCards.values.map { - val dbCard = update(it.toCardEntity()).toMemDbCard().copy(changedAt = timestamp) - database.saveCard(dbCard).toCardEntity() + override fun updateCard(cardEntity: DbCard): DbCard { + validateCardEntityForUpdate(cardEntity) + val found = database.findCardById(cardEntity.cardId.toLong()) + ?: throw DbDataException("Can't find card, id = ${cardEntity.cardId.toLong()}") + if (found.dictionaryId != cardEntity.dictionaryId.toLong()) { + throw DbDataException("Changing dictionary-id is not allowed; card id = ${cardEntity.cardId.toLong()}") } - val dictionaries = dbDictionaries.values.map { it.toDictionaryEntity() } - return CardsDbResponse( - cards = cards, - dictionaries = dictionaries, - errors = emptyList(), - ) - } - - override fun resetCard(userId: AppUserId, cardId: CardId): CardDbResponse { val timestamp = systemNow() - val card = - database.findCardById(cardId.asLong()) ?: return CardDbResponse(noCardFoundDbError("resetCard", cardId)) - val errors = mutableListOf() - checkDictionaryUser("resetCard", userId, card.dictionaryId.asDictionaryId(), cardId, errors) - if (errors.isNotEmpty()) { - return CardDbResponse(errors = errors) - } - return CardDbResponse(card = database.saveCard(card.copy(answered = 0, changedAt = timestamp)).toCardEntity()) + return database.saveCard(cardEntity.toMemDbCard().copy(changedAt = timestamp)).toDbCard() } - override fun removeCard(userId: AppUserId, cardId: CardId): RemoveCardDbResponse { + override fun deleteCard(cardId: String): DbCard { val timestamp = systemNow() - val card = database.findCardById(cardId.asLong()) ?: return RemoveCardDbResponse( - noCardFoundDbError("removeCard", cardId) - ) - val errors = mutableListOf() - checkDictionaryUser("removeCard", userId, card.dictionaryId.asDictionaryId(), cardId, errors) - if (errors.isNotEmpty()) { - return RemoveCardDbResponse(errors = errors) - } - if (!database.deleteCardById(cardId.asLong())) { - return RemoveCardDbResponse(noCardFoundDbError("removeCard", cardId)) - } - return RemoveCardDbResponse(card = card.copy(changedAt = timestamp).toCardEntity()) - } - - @Suppress("DuplicatedCode") - private fun checkDictionaryUser( - operation: String, - userId: AppUserId, - dictionaryId: DictionaryId, - entityId: Id, - errors: MutableList - ): MemDbDictionary? { - val dictionary = database.findDictionaryById(dictionaryId.asLong()) - if (dictionary == null) { - errors.add(noDictionaryFoundDbError(operation, dictionaryId)) - return null - } - - if (dictionary.userId == userId.asLong()) { - return dictionary + val found = database.findCardById(cardId.toLong()) + ?: throw DbDataException("Can't find card, id = ${cardId.toLong()}") + if (!database.deleteCardById(cardId.toLong())) { + throw DbDataException("Can't delete card, id = ${cardId.toLong()}") } - errors.add(forbiddenEntityDbError(operation, entityId, userId)) - return null + return found.copy(changedAt = timestamp).toDbCard() } } \ No newline at end of file diff --git a/db-mem/src/main/kotlin/MemDbDictionaryRepository.kt b/db-mem/src/main/kotlin/MemDbDictionaryRepository.kt index 4fe4cef5..73c0b4d6 100644 --- a/db-mem/src/main/kotlin/MemDbDictionaryRepository.kt +++ b/db-mem/src/main/kotlin/MemDbDictionaryRepository.kt @@ -1,113 +1,34 @@ package com.gitlab.sszuev.flashcards.dbmem -import com.gitlab.sszuev.flashcards.common.SysConfig -import com.gitlab.sszuev.flashcards.common.answered -import com.gitlab.sszuev.flashcards.common.asLong -import com.gitlab.sszuev.flashcards.common.documents.createReader -import com.gitlab.sszuev.flashcards.common.documents.createWriter -import com.gitlab.sszuev.flashcards.common.forbiddenEntityDbError -import com.gitlab.sszuev.flashcards.common.noDictionaryFoundDbError -import com.gitlab.sszuev.flashcards.common.status -import com.gitlab.sszuev.flashcards.common.systemNow -import com.gitlab.sszuev.flashcards.common.wrongResourceDbError -import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbDictionary -import com.gitlab.sszuev.flashcards.model.common.AppError -import com.gitlab.sszuev.flashcards.model.common.AppUserId -import com.gitlab.sszuev.flashcards.model.domain.DictionaryEntity -import com.gitlab.sszuev.flashcards.model.domain.DictionaryId -import com.gitlab.sszuev.flashcards.model.domain.ResourceEntity +import com.gitlab.sszuev.flashcards.repositories.DbDataException +import com.gitlab.sszuev.flashcards.repositories.DbDictionary import com.gitlab.sszuev.flashcards.repositories.DbDictionaryRepository -import com.gitlab.sszuev.flashcards.repositories.DictionariesDbResponse -import com.gitlab.sszuev.flashcards.repositories.DictionaryDbResponse -import com.gitlab.sszuev.flashcards.repositories.ImportDictionaryDbResponse -import com.gitlab.sszuev.flashcards.repositories.RemoveDictionaryDbResponse +import com.gitlab.sszuev.flashcards.systemNow class MemDbDictionaryRepository( dbConfig: MemDbConfig = MemDbConfig(), - private val sysConfig: SysConfig = SysConfig(), ) : DbDictionaryRepository { private val database = MemDatabase.get(databaseLocation = dbConfig.dataLocation) - override fun getAllDictionaries(userId: AppUserId): DictionariesDbResponse { - val dictionaries = this.database.findDictionariesByUserId(userId.asLong()) - return DictionariesDbResponse(dictionaries = dictionaries.map { it.toDictionaryEntity() }.toList()) - } + override fun findDictionaryById(dictionaryId: String): DbDictionary? = + database.findDictionaryById(dictionaryId.toLong())?.toDbDictionary() - override fun createDictionary(userId: AppUserId, entity: DictionaryEntity): DictionaryDbResponse { - val timestamp = systemNow() - val dictionary = - database.saveDictionary(entity.toMemDbDictionary().copy(userId = userId.asLong(), changedAt = timestamp)) - return DictionaryDbResponse(dictionary = dictionary.toDictionaryEntity()) - } + override fun findDictionariesByUserId(userId: String): Sequence = + this.database.findDictionariesByUserId(userId).map { it.toDbDictionary() } - override fun removeDictionary(userId: AppUserId, dictionaryId: DictionaryId): RemoveDictionaryDbResponse { - val timestamp = systemNow() - val errors = mutableListOf() - val found = checkDictionaryUser("removeDictionary", userId, dictionaryId, errors) - if (errors.isNotEmpty()) { - return RemoveDictionaryDbResponse(errors = errors) - } - if (!database.deleteDictionaryById(dictionaryId.asLong())) { - return RemoveDictionaryDbResponse(noDictionaryFoundDbError("removeDictionary", dictionaryId)) - } - return RemoveDictionaryDbResponse( - dictionary = checkNotNull(found).copy(changedAt = timestamp).toDictionaryEntity() - ) - } + override fun createDictionary(entity: DbDictionary): DbDictionary = + database.saveDictionary(entity.toMemDbDictionary().copy(changedAt = systemNow())).toDbDictionary() - override fun importDictionary( - userId: AppUserId, - dictionaryId: DictionaryId - ): ImportDictionaryDbResponse { - val errors = mutableListOf() - val found = checkDictionaryUser("importDictionary", userId, dictionaryId, errors) - if (errors.isNotEmpty()) { - return ImportDictionaryDbResponse(errors = errors) - } - checkNotNull(found) - val cards = database.findCardsByDictionaryId(checkNotNull(found.id)).toList() - val document = fromDatabaseToDocumentDictionary(found, cards) { sysConfig.status(it) } - val res = try { - createWriter().write(document) - } catch (ex: Exception) { - return ImportDictionaryDbResponse(wrongResourceDbError(ex)) + override fun deleteDictionary(dictionaryId: String): DbDictionary { + require(dictionaryId.isNotBlank()) + val id = dictionaryId.toLong() + val found = database.findDictionaryById(id)?.toDbDictionary() + ?: throw DbDataException("Can't find dictionary $id") + if (!database.deleteDictionaryById(id)) { + throw DbDataException("Can't delete dictionary $id") } - return ImportDictionaryDbResponse(resource = ResourceEntity(resourceId = dictionaryId, data = res)) + return found } - override fun exportDictionary(userId: AppUserId, resource: ResourceEntity): DictionaryDbResponse { - val timestamp = systemNow() - val dictionaryDocument = try { - createReader().parse(resource.data) - } catch (ex: Exception) { - return DictionaryDbResponse(wrongResourceDbError(ex)) - } - val dictionary = database.saveDictionary( - dictionaryDocument.toMemDbDictionary().copy(userId = userId.asLong(), changedAt = timestamp) - ) - dictionaryDocument.toMemDbCards { sysConfig.answered(it) }.forEach { - database.saveCard(it.copy(dictionaryId = dictionary.id, changedAt = timestamp)) - } - return DictionaryDbResponse(dictionary = dictionary.toDictionaryEntity()) - } - - @Suppress("DuplicatedCode") - private fun checkDictionaryUser( - operation: String, - userId: AppUserId, - dictionaryId: DictionaryId, - errors: MutableList - ): MemDbDictionary? { - val dictionary = database.findDictionaryById(dictionaryId.asLong()) - if (dictionary == null) { - errors.add(noDictionaryFoundDbError(operation, dictionaryId)) - return null - } - if (dictionary.userId == userId.asLong()) { - return dictionary - } - errors.add(forbiddenEntityDbError(operation, dictionaryId, userId)) - return null - } } \ No newline at end of file diff --git a/db-mem/src/main/kotlin/MemDbEntityMapper.kt b/db-mem/src/main/kotlin/MemDbEntityMapper.kt index 1a483f70..59a4e341 100644 --- a/db-mem/src/main/kotlin/MemDbEntityMapper.kt +++ b/db-mem/src/main/kotlin/MemDbEntityMapper.kt @@ -1,54 +1,29 @@ package com.gitlab.sszuev.flashcards.dbmem +import com.gitlab.sszuev.flashcards.asJava +import com.gitlab.sszuev.flashcards.asKotlin import com.gitlab.sszuev.flashcards.common.CommonCardDetailsDto import com.gitlab.sszuev.flashcards.common.CommonDictionaryDetailsDto import com.gitlab.sszuev.flashcards.common.CommonExampleDto -import com.gitlab.sszuev.flashcards.common.CommonUserDetailsDto import com.gitlab.sszuev.flashcards.common.CommonWordDto -import com.gitlab.sszuev.flashcards.common.LanguageRepository -import com.gitlab.sszuev.flashcards.common.asJava -import com.gitlab.sszuev.flashcards.common.asKotlin -import com.gitlab.sszuev.flashcards.common.asLong import com.gitlab.sszuev.flashcards.common.detailsAsCommonCardDetailsDto -import com.gitlab.sszuev.flashcards.common.documents.DocumentCard -import com.gitlab.sszuev.flashcards.common.documents.DocumentCardStatus -import com.gitlab.sszuev.flashcards.common.documents.DocumentDictionary import com.gitlab.sszuev.flashcards.common.parseCardDetailsJson import com.gitlab.sszuev.flashcards.common.parseCardWordsJson import com.gitlab.sszuev.flashcards.common.parseDictionaryDetailsJson -import com.gitlab.sszuev.flashcards.common.parseUserDetailsJson import com.gitlab.sszuev.flashcards.common.toCardEntityDetails import com.gitlab.sszuev.flashcards.common.toCardEntityStats import com.gitlab.sszuev.flashcards.common.toCardWordEntity -import com.gitlab.sszuev.flashcards.common.toCommonWordDtoList -import com.gitlab.sszuev.flashcards.common.toDocumentExamples -import com.gitlab.sszuev.flashcards.common.toDocumentTranslations import com.gitlab.sszuev.flashcards.common.toJsonString import com.gitlab.sszuev.flashcards.common.wordsAsCommonWordDtoList import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbCard import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbDictionary import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbExample import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbLanguage -import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbUser import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbWord -import com.gitlab.sszuev.flashcards.model.common.AppAuthId -import com.gitlab.sszuev.flashcards.model.common.AppUserEntity -import com.gitlab.sszuev.flashcards.model.common.AppUserId -import com.gitlab.sszuev.flashcards.model.domain.CardEntity -import com.gitlab.sszuev.flashcards.model.domain.CardId -import com.gitlab.sszuev.flashcards.model.domain.DictionaryEntity -import com.gitlab.sszuev.flashcards.model.domain.DictionaryId -import com.gitlab.sszuev.flashcards.model.domain.LangEntity -import com.gitlab.sszuev.flashcards.model.domain.LangId -import java.util.UUID - -internal fun MemDbUser.detailsAsJsonString(): String { - return CommonUserDetailsDto(details).toJsonString() -} - -internal fun fromJsonStringToMemDbUserDetails(json: String): Map { - return parseUserDetailsJson(json).mapValues { it.toString() } -} +import com.gitlab.sszuev.flashcards.repositories.DbCard +import com.gitlab.sszuev.flashcards.repositories.DbDictionary +import com.gitlab.sszuev.flashcards.repositories.DbLang +import com.gitlab.sszuev.flashcards.repositories.LanguageRepository internal fun MemDbDictionary.detailsAsJsonString(): String { return CommonDictionaryDetailsDto(this.details).toJsonString() @@ -74,81 +49,28 @@ internal fun fromJsonStringToMemDbWords(json: String): List { return parseCardWordsJson(json).map { it.toMemDbWord() } } -internal fun MemDbUser.toAppUserEntity(): AppUserEntity = AppUserEntity( - id = id?.asUserId() ?: AppUserId.NONE, - authId = uuid.asAppAuthId(), -) - -internal fun DocumentDictionary.toMemDbCards(mapAnswered: (DocumentCardStatus) -> Int): List { - return this.cards.map { it.toMemDbCard(mapAnswered) } -} - -internal fun DocumentDictionary.toMemDbDictionary(): MemDbDictionary { - return MemDbDictionary( - name = this.name, - sourceLanguage = createMemDbLanguage(this.sourceLang), - targetLanguage = createMemDbLanguage(this.targetLang), - details = emptyMap(), - ) -} - -internal fun fromDatabaseToDocumentDictionary( - dictionary: MemDbDictionary, - cards: List, - mapStatus: (Int?) -> DocumentCardStatus -): DocumentDictionary { - return DocumentDictionary( - name = dictionary.name, - sourceLang = dictionary.sourceLanguage.id, - targetLang = dictionary.targetLanguage.id, - cards = cards.map { it.toDocumentCard(mapStatus) }, - ) -} - -internal fun DocumentCard.toMemDbCard(mapAnswered: (DocumentCardStatus) -> Int): MemDbCard { - return MemDbCard( - words = this.toMemDbWords(), - details = emptyMap(), - answered = mapAnswered(this.status), - ) -} - -private fun DocumentCard.toMemDbWords(): List { - return toCommonWordDtoList().map { it.toMemDbWord() } -} - -internal fun MemDbCard.toDocumentCard(mapStatus: (Int?) -> DocumentCardStatus): DocumentCard { - val word = this.words.first().toCommonWordDto() - return DocumentCard( - text = word.word, - transcription = word.transcription, - partOfSpeech = word.partOfSpeech, - translations = word.toDocumentTranslations(), - examples = word.toDocumentExamples(), - status = mapStatus(this.answered), - ) -} - -internal fun MemDbDictionary.toDictionaryEntity(): DictionaryEntity = DictionaryEntity( - dictionaryId = this.id?.asDictionaryId() ?: DictionaryId.NONE, +internal fun MemDbDictionary.toDbDictionary() = DbDictionary( + dictionaryId = this.id?.toString() ?: "", + userId = this.userId ?: "", name = this.name, - sourceLang = this.sourceLanguage.toLangEntity(), - targetLang = this.targetLanguage.toLangEntity(), + sourceLang = this.sourceLanguage.toDbLang(), + targetLang = this.targetLanguage.toDbLang(), ) -internal fun DictionaryEntity.toMemDbDictionary(): MemDbDictionary = MemDbDictionary( - id = if (this.dictionaryId == DictionaryId.NONE) null else this.dictionaryId.asLong(), +internal fun DbDictionary.toMemDbDictionary(): MemDbDictionary = MemDbDictionary( + id = if (this.dictionaryId.isBlank()) null else this.dictionaryId.toLong(), name = this.name, sourceLanguage = this.sourceLang.toMemDbLanguage(), targetLanguage = this.targetLang.toMemDbLanguage(), details = emptyMap(), + userId = this.userId.ifBlank { null } ) -internal fun MemDbCard.toCardEntity(): CardEntity { +internal fun MemDbCard.toDbCard(): DbCard { val details: CommonCardDetailsDto = this.detailsAsCommonCardDetailsDto() - return CardEntity( - cardId = id?.asCardId() ?: CardId.NONE, - dictionaryId = dictionaryId?.asDictionaryId() ?: DictionaryId.NONE, + return DbCard( + cardId = id?.toString() ?: "", + dictionaryId = dictionaryId?.toString() ?: "", words = this.words.map { it.toCommonWordDto() }.map { it.toCardWordEntity() }, details = details.toCardEntityDetails(), stats = details.toCardEntityStats(), @@ -157,10 +79,10 @@ internal fun MemDbCard.toCardEntity(): CardEntity { ) } -internal fun CardEntity.toMemDbCard(): MemDbCard { - val dictionaryId = dictionaryId.asLong() +internal fun DbCard.toMemDbCard(): MemDbCard { + val dictionaryId = dictionaryId.toLong() return MemDbCard( - id = if (this.cardId == CardId.NONE) null else this.cardId.asLong(), + id = if (this.cardId.isBlank()) null else this.cardId.toLong(), dictionaryId = dictionaryId, words = this.wordsAsCommonWordDtoList().map { it.toMemDbWord() }, details = this.detailsAsCommonCardDetailsDto().toMemDbCardDetails(), @@ -169,18 +91,18 @@ internal fun CardEntity.toMemDbCard(): MemDbCard { ) } -internal fun MemDbLanguage.toLangEntity(): LangEntity = LangEntity( - langId = this.id.asLangId(), - partsOfSpeech = this.partsOfSpeech, -) - internal fun createMemDbLanguage(tag: String): MemDbLanguage = MemDbLanguage( id = tag, partsOfSpeech = LanguageRepository.partsOfSpeech(tag) ) -internal fun LangEntity.toMemDbLanguage(): MemDbLanguage = MemDbLanguage( - id = this.langId.asString(), +internal fun DbLang.toMemDbLanguage(): MemDbLanguage = MemDbLanguage( + id = this.langId, + partsOfSpeech = this.partsOfSpeech, +) + +internal fun MemDbLanguage.toDbLang(): DbLang = DbLang( + langId = this.id, partsOfSpeech = this.partsOfSpeech, ) @@ -213,15 +135,3 @@ private fun CommonExampleDto.toMemDbExample(): MemDbExample = MemDbExample( internal fun MemDbCard.detailsAsCommonCardDetailsDto(): CommonCardDetailsDto = CommonCardDetailsDto(this.details) private fun CommonCardDetailsDto.toMemDbCardDetails(): Map = this.mapValues { it.value.toString() } - -private fun Long.asUserId(): AppUserId = AppUserId(toString()) - -private fun String.asLangId(): LangId = LangId(this) - -internal fun Long.asCardId(): CardId = CardId(toString()) - -internal fun Long.asDictionaryId(): DictionaryId = DictionaryId(toString()) - -internal fun Long?.asDictionaryId(): DictionaryId = DictionaryId(checkNotNull(this).toString()) - -private fun UUID.asAppAuthId(): AppAuthId = AppAuthId(toString()) \ No newline at end of file diff --git a/db-mem/src/main/kotlin/MemDbUserRepository.kt b/db-mem/src/main/kotlin/MemDbUserRepository.kt deleted file mode 100644 index 16954c27..00000000 --- a/db-mem/src/main/kotlin/MemDbUserRepository.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.gitlab.sszuev.flashcards.dbmem - -import com.gitlab.sszuev.flashcards.common.noUserFoundDbError -import com.gitlab.sszuev.flashcards.common.wrongUserUuidDbError -import com.gitlab.sszuev.flashcards.model.common.AppAuthId -import com.gitlab.sszuev.flashcards.model.common.AppUserEntity -import com.gitlab.sszuev.flashcards.repositories.DbUserRepository -import com.gitlab.sszuev.flashcards.repositories.UserEntityDbResponse -import java.util.UUID - -class MemDbUserRepository( - dbConfig: MemDbConfig = MemDbConfig(), -) : DbUserRepository { - - private val database = MemDatabase.get(dbConfig.dataLocation) - - override fun getUser(authId: AppAuthId): UserEntityDbResponse { - val uuid = try { - UUID.fromString(authId.asString()) - } catch (ex: IllegalArgumentException) { - return UserEntityDbResponse( - user = AppUserEntity.EMPTY, errors = listOf(wrongUserUuidDbError("getUser", authId)) - ) - } - val res = database.findUserByUuid(uuid) - ?: return UserEntityDbResponse( - user = AppUserEntity.EMPTY, errors = listOf(noUserFoundDbError("getUser", authId)) - ) - return UserEntityDbResponse(user = res.toAppUserEntity()) - } -} \ No newline at end of file diff --git a/db-mem/src/main/kotlin/dao/MemDbDictionary.kt b/db-mem/src/main/kotlin/dao/MemDbDictionary.kt index 2ac84a29..04fcd963 100644 --- a/db-mem/src/main/kotlin/dao/MemDbDictionary.kt +++ b/db-mem/src/main/kotlin/dao/MemDbDictionary.kt @@ -10,7 +10,7 @@ data class MemDbDictionary( val sourceLanguage: MemDbLanguage, val targetLanguage: MemDbLanguage, val details: Map = emptyMap(), - val userId: Long? = null, + val userId: String? = null, val id: Long? = null, val changedAt: LocalDateTime? = null, ) \ No newline at end of file diff --git a/db-mem/src/main/kotlin/dao/MemDbUser.kt b/db-mem/src/main/kotlin/dao/MemDbUser.kt deleted file mode 100644 index 1787d01c..00000000 --- a/db-mem/src/main/kotlin/dao/MemDbUser.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.gitlab.sszuev.flashcards.dbmem.dao - -import java.time.LocalDateTime -import java.util.UUID - -data class MemDbUser( - val id: Long?, - val uuid: UUID, - val details: Map = emptyMap(), - val changedAt: LocalDateTime? = null, -) \ No newline at end of file diff --git a/db-mem/src/test/kotlin/EntityMapperTest.kt b/db-mem/src/test/kotlin/EntityMapperTest.kt deleted file mode 100644 index 44d16ec4..00000000 --- a/db-mem/src/test/kotlin/EntityMapperTest.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.gitlab.sszuev.flashcards.dbmem - -import com.gitlab.sszuev.flashcards.common.documents.DocumentCard -import com.gitlab.sszuev.flashcards.common.documents.DocumentCardStatus -import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbCard -import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbExample -import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbWord -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test - -internal class EntityMapperTest { - - companion object { - private val testDocumentCard = DocumentCard( - text = "snowfall", - transcription = "ˈsnəʊfɔːl", - partOfSpeech = "noun", - translations = listOf("снегопад"), - examples = listOf( - "Due to the heavy snowfall, all flights have been cancelled... -- Из-за сильного снегопада все рейсы отменены...", - "It's the first snowfall of Christmas.", - ), - status = DocumentCardStatus.LEARNED, - ) - - private val testMemDbCard = MemDbCard( - details = emptyMap(), - words = listOf( - MemDbWord( - word = "snowfall", - transcription = "ˈsnəʊfɔːl", - partOfSpeech = "noun", - translations = listOf(listOf("снегопад")), - examples = listOf( - MemDbExample( - translation = "Из-за сильного снегопада все рейсы отменены...", - text = "Due to the heavy snowfall, all flights have been cancelled...", - ), - MemDbExample(text = "It's the first snowfall of Christmas.") - ) - ) - ), - answered = 42, - ) - } - - @Test - fun `test map document-card to mem-db-card`() { - val givenCard = testDocumentCard.copy(status = DocumentCardStatus.LEARNED) - val actualCard = givenCard.toMemDbCard { - if (it == givenCard.status) testMemDbCard.answered!! else throw AssertionError() - } - Assertions.assertEquals(testMemDbCard, actualCard) - } - - @Test - fun `test map mem-db-card to document-card`() { - val givenCard = testMemDbCard.copy(id = 1, dictionaryId = 2, answered = 42, details = mapOf("a" to "b")) - val actualCard = givenCard.toDocumentCard { - if (it == givenCard.answered) testDocumentCard.status else throw AssertionError() - } - Assertions.assertEquals(testDocumentCard, actualCard) - } - -} \ No newline at end of file diff --git a/db-mem/src/test/kotlin/MemDatabaseTest.kt b/db-mem/src/test/kotlin/MemDatabaseTest.kt index cd61b870..16dc650e 100644 --- a/db-mem/src/test/kotlin/MemDatabaseTest.kt +++ b/db-mem/src/test/kotlin/MemDatabaseTest.kt @@ -1,8 +1,6 @@ package com.gitlab.sszuev.flashcards.dbmem -import com.gitlab.sszuev.flashcards.common.systemNow import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbCard -import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbUser import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbWord import com.gitlab.sszuev.flashcards.dbmem.testutils.classPathResourceDir import com.gitlab.sszuev.flashcards.dbmem.testutils.copyClassPathDataToDir @@ -11,7 +9,6 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Path -import java.time.LocalDateTime import java.util.UUID @Order(1) @@ -19,66 +16,18 @@ internal class MemDatabaseTest { companion object { private val existingUUID: UUID = UUID.fromString("c9a414f5-3f75-4494-b664-f4c8b33ff4e6") - private val newUUID: UUID = UUID.fromString("45a34bd8-5472-491e-8e27-84290314ee38") - private val timestamp: LocalDateTime = LocalDateTime.parse("2022-12-26T16:04:14") - private val existingUser = MemDbUser( - id = 42, - uuid = existingUUID, - changedAt = timestamp, - details = emptyMap(), - ) - private val testUser = MemDbUser( - id = null, - uuid = newUUID, - changedAt = null, - details = emptyMap(), - ) private val testCard = MemDbCard( id = null, words = listOf(MemDbWord(word = "word", translations = listOf(listOf("слово")))), ) - - private fun LocalDateTime.isAfterOrEqual(other: LocalDateTime): Boolean { - return this == other || isAfter(other) - } } @Test fun `test find users`() { val database = MemDatabase.load("classpath:$classPathResourceDir") - val users = database.findUsers().toList() - Assertions.assertEquals(listOf(existingUser), users) - } - - @Test - fun `test load from directory & reload & find-users & find-user-by-uuid & save-user`(@TempDir dir: Path) { - val timestamp = systemNow() - copyClassPathDataToDir(classPathResourceDir, dir) - val database1 = MemDatabase.get(dir.toString()) - Assertions.assertEquals(listOf(existingUser), database1.findUsers().toList()) - val newUser = database1.saveUser(testUser) - Assertions.assertEquals(43, newUser.id) - Assertions.assertTrue(newUser.changedAt!!.isAfterOrEqual(timestamp)) - Assertions.assertEquals(listOf(existingUser, newUser), database1.findUsers().toList()) - - // test cleaned: wait 0.5 second (test period is 200 ms) and reload store - Thread.sleep(500) - MemDatabase.clear() - - val database2 = MemDatabase.get(dir.toString()) - Assertions.assertNotSame(database1, database2) - Assertions.assertEquals(2, database2.countUsers()) - Assertions.assertEquals(existingUser, database2.findUserByUuid(existingUUID)!!) - Assertions.assertEquals(newUser, database2.findUserByUuid(newUUID)!!) - - val database3 = MemDatabase.load(dir.toString()) - Assertions.assertNotSame(database1, database3) - Assertions.assertEquals(listOf(existingUser, newUser), database3.findUsers().toList()) - Assertions.assertEquals(existingUser, database3.findUserByUuid(existingUUID)!!) - Assertions.assertEquals(newUser, database3.findUserByUuid(newUUID)!!) - - MemDatabase.clear() + val users = database.findUserIds().toList() + Assertions.assertEquals(listOf(existingUUID.toString()), users) } @Test diff --git a/db-mem/src/test/kotlin/MemDbUserRepositoryTest.kt b/db-mem/src/test/kotlin/MemDbUserRepositoryTest.kt deleted file mode 100644 index 39bd33de..00000000 --- a/db-mem/src/test/kotlin/MemDbUserRepositoryTest.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.gitlab.sszuev.flashcards.dbmem - -import com.gitlab.sszuev.flashcards.dbcommon.DbUserRepositoryTest -import com.gitlab.sszuev.flashcards.repositories.DbUserRepository -import org.junit.jupiter.api.Order - -@Order(1) -internal class MemDbUserRepositoryTest : DbUserRepositoryTest() { - override val repository: DbUserRepository = MemDbUserRepository() -} \ No newline at end of file diff --git a/db-mem/src/test/resources/db-mem-test-data/dictionaries.csv b/db-mem/src/test/resources/db-mem-test-data/dictionaries.csv index 3b73692b..466e7a37 100644 --- a/db-mem/src/test/resources/db-mem-test-data/dictionaries.csv +++ b/db-mem/src/test/resources/db-mem-test-data/dictionaries.csv @@ -1,3 +1,3 @@ id,name,user_id,source_lang,target_lang,details,changed_at -1,Irregular Verbs,42,en,ru,{},2022-12-26T16:04:14 -2,Weather,42,en,ru,{},2022-12-26T16:04:14 \ No newline at end of file +1,Irregular Verbs,c9a414f5-3f75-4494-b664-f4c8b33ff4e6,en,ru,{},2022-12-26T16:04:14 +2,Weather,c9a414f5-3f75-4494-b664-f4c8b33ff4e6,en,ru,{},2022-12-26T16:04:14 \ No newline at end of file diff --git a/db-mem/src/test/resources/db-mem-test-data/users.csv b/db-mem/src/test/resources/db-mem-test-data/users.csv deleted file mode 100644 index 21c35afe..00000000 --- a/db-mem/src/test/resources/db-mem-test-data/users.csv +++ /dev/null @@ -1,2 +0,0 @@ -id,uuid,details,changed_at -42,c9a414f5-3f75-4494-b664-f4c8b33ff4e6,{},2022-12-26T16:04:14 \ No newline at end of file diff --git a/db-pg/src/main/kotlin/PgDbCardRepository.kt b/db-pg/src/main/kotlin/PgDbCardRepository.kt index d84ca34a..be0eb4d5 100644 --- a/db-pg/src/main/kotlin/PgDbCardRepository.kt +++ b/db-pg/src/main/kotlin/PgDbCardRepository.kt @@ -1,42 +1,23 @@ package com.gitlab.sszuev.flashcards.dbpg -import com.gitlab.sszuev.flashcards.common.SysConfig -import com.gitlab.sszuev.flashcards.common.asLong -import com.gitlab.sszuev.flashcards.common.dbError -import com.gitlab.sszuev.flashcards.common.forbiddenEntityDbError -import com.gitlab.sszuev.flashcards.common.noCardFoundDbError -import com.gitlab.sszuev.flashcards.common.noDictionaryFoundDbError -import com.gitlab.sszuev.flashcards.common.systemNow +import com.gitlab.sszuev.flashcards.asKotlin +import com.gitlab.sszuev.flashcards.common.detailsAsCommonCardDetailsDto +import com.gitlab.sszuev.flashcards.common.toJsonString import com.gitlab.sszuev.flashcards.common.validateCardEntityForCreate import com.gitlab.sszuev.flashcards.common.validateCardEntityForUpdate import com.gitlab.sszuev.flashcards.dbpg.dao.Cards -import com.gitlab.sszuev.flashcards.dbpg.dao.Dictionaries import com.gitlab.sszuev.flashcards.dbpg.dao.PgDbCard -import com.gitlab.sszuev.flashcards.dbpg.dao.PgDbDictionary -import com.gitlab.sszuev.flashcards.model.Id -import com.gitlab.sszuev.flashcards.model.common.AppError -import com.gitlab.sszuev.flashcards.model.common.AppUserId -import com.gitlab.sszuev.flashcards.model.domain.CardEntity -import com.gitlab.sszuev.flashcards.model.domain.CardFilter -import com.gitlab.sszuev.flashcards.model.domain.CardId -import com.gitlab.sszuev.flashcards.model.domain.DictionaryId -import com.gitlab.sszuev.flashcards.repositories.CardDbResponse -import com.gitlab.sszuev.flashcards.repositories.CardsDbResponse +import com.gitlab.sszuev.flashcards.repositories.DbCard import com.gitlab.sszuev.flashcards.repositories.DbCardRepository -import com.gitlab.sszuev.flashcards.repositories.RemoveCardDbResponse -import org.jetbrains.exposed.sql.CustomFunction -import org.jetbrains.exposed.sql.DoubleColumnType -import org.jetbrains.exposed.sql.Op -import org.jetbrains.exposed.sql.SortOrder +import com.gitlab.sszuev.flashcards.repositories.DbDataException +import com.gitlab.sszuev.flashcards.systemNow import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList -import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.deleteWhere -import org.jetbrains.exposed.sql.or +import org.jetbrains.exposed.sql.statements.BatchUpdateStatement +import org.jetbrains.exposed.sql.transactions.TransactionManager class PgDbCardRepository( dbConfig: PgDbConfig = PgDbConfig(), - private val sysConfig: SysConfig = SysConfig(), ) : DbCardRepository { private val connection by lazy { // lazy, to avoid initialization error when there is no real pg-database @@ -44,201 +25,94 @@ class PgDbCardRepository( PgDbConnector.connection(dbConfig) } - override fun getCard(userId: AppUserId, cardId: CardId): CardDbResponse { + override fun findCardById(cardId: String): DbCard? { + require(cardId.isNotBlank()) return connection.execute { - val card = PgDbCard.findById(cardId.asLong()) ?: return@execute CardDbResponse( - noCardFoundDbError(operation = "getCard", id = cardId) - ) - val errors = mutableListOf() - checkDictionaryUser("getCard", userId, checkNotNull(card.dictionaryId).asDictionaryId(), cardId, errors) - if (errors.isNotEmpty()) { - return@execute CardDbResponse(errors = errors) - } - CardDbResponse(card = card.toCardEntity()) + PgDbCard.findById(cardId.toLong())?.toCardEntity() } } - override fun getAllCards(userId: AppUserId, dictionaryId: DictionaryId): CardsDbResponse { + override fun findCardsByDictionaryId(dictionaryId: String): Sequence { + require(dictionaryId.isNotBlank()) return connection.execute { - val errors = mutableListOf() - val dictionary = checkDictionaryUser("getAllCards", userId, dictionaryId, dictionaryId, errors) - if (errors.isNotEmpty() || dictionary == null) { - return@execute CardsDbResponse(errors = errors) - } - val cards = PgDbCard.find { Cards.dictionaryId eq dictionaryId.asLong() }.map { it.toCardEntity() } - val dictionaries = listOf(dictionary.toDictionaryEntity()) - CardsDbResponse( - cards = cards, - dictionaries = dictionaries, - errors = emptyList(), - ) + PgDbCard.find { Cards.dictionaryId eq dictionaryId.toLong() }.map { it.toCardEntity() }.asSequence() } } - override fun searchCard(userId: AppUserId, filter: CardFilter): CardsDbResponse { - require(filter.length != 0) { "zero length is specified" } - val dictionaryIds = filter.dictionaryIds.map { it.asLong() } - val learned = sysConfig.numberOfRightAnswers - val random = CustomFunction("random", DoubleColumnType()) + override fun findCardsByDictionaryIdIn(dictionaryIds: Iterable): Sequence { return connection.execute { - val dictionariesFromDb = PgDbDictionary.find(Dictionaries.id inList dictionaryIds) - if (dictionariesFromDb.empty()) { - return@execute CardsDbResponse(cards = emptyList(), dictionaries = emptyList(), errors = emptyList()) - } - val forbiddenIds = - dictionariesFromDb.filter { it.userId.value != userId.asLong() }.map { it.id.value }.toSet() - val errors = forbiddenIds.map { forbiddenEntityDbError("searchCards", it.asDictionaryId(), userId) } - if (errors.isNotEmpty()) { - return@execute CardsDbResponse(cards = emptyList(), errors = errors) - } - val dictionaries = - dictionariesFromDb.filterNot { it.id.value in forbiddenIds }.map { it.toDictionaryEntity() } - var cardsIterable = PgDbCard.find { - Cards.dictionaryId inList dictionaryIds and - (if (filter.withUnknown) Op.TRUE else Cards.answered.isNull() or Cards.answered.lessEq(learned)) - }.orderBy(random to SortOrder.ASC) - .orderBy(Cards.dictionaryId to SortOrder.ASC) - if (filter.length > 0) { - cardsIterable = cardsIterable.limit(filter.length) - } - val cards = cardsIterable.map { it.toCardEntity() } - CardsDbResponse(cards = cards, dictionaries = dictionaries) + PgDbCard.find { + Cards.dictionaryId inList + dictionaryIds.onEach { require(it.isNotBlank()) }.map { it.toDictionariesId() }.toSet() + }.map { it.toCardEntity() }.asSequence() } } - override fun createCard(userId: AppUserId, cardEntity: CardEntity): CardDbResponse { + override fun findCardsByIdIn(cardIds: Iterable): Sequence { return connection.execute { - validateCardEntityForCreate(cardEntity) - val errors = mutableListOf() - checkDictionaryUser("createCard", userId, cardEntity.dictionaryId, cardEntity.dictionaryId, errors) - if (errors.isNotEmpty()) { - return@execute CardDbResponse(errors = errors) - } - val timestamp = systemNow() - val res = PgDbCard.new { - writeCardEntityToPgDbCard(from = cardEntity, to = this, timestamp = timestamp) - } - CardDbResponse(card = res.toCardEntity()) + PgDbCard.find { + Cards.id inList cardIds.onEach { require(it.isNotBlank()) }.map { it.toCardsId() }.toSet() + }.map { it.toCardEntity() }.asSequence() } } - override fun updateCard(userId: AppUserId, cardEntity: CardEntity): CardDbResponse { + override fun createCard(cardEntity: DbCard): DbCard { + validateCardEntityForCreate(cardEntity) return connection.execute { - validateCardEntityForUpdate(cardEntity) val timestamp = systemNow() - val found = PgDbCard.findById(cardEntity.cardId.asRecordId()) ?: return@execute CardDbResponse( - noCardFoundDbError("updateCard", cardEntity.cardId) - ) - val errors = mutableListOf() - val foundDictionary = - checkDictionaryUser("updateCard", userId, cardEntity.dictionaryId, cardEntity.cardId, errors) - if (foundDictionary != null && foundDictionary.id.value != cardEntity.dictionaryId.asLong()) { - errors.add( - dbError( - operation = "updateCard", - fieldName = cardEntity.cardId.asString(), - details = "given and found dictionary ids do not match: ${cardEntity.dictionaryId.asString()} != ${found.dictionaryId.value}" - ) - ) - } - if (errors.isNotEmpty()) { - return@execute CardDbResponse(errors = errors) + try { + PgDbCard.new { + writeCardEntityToPgDbCard(from = cardEntity, to = this, timestamp = timestamp) + }.toCardEntity() + } catch (ex: Exception) { + throw DbDataException("Can't create card $cardEntity", ex) } - writeCardEntityToPgDbCard(from = cardEntity, to = found, timestamp = timestamp) - return@execute CardDbResponse(card = found.toCardEntity()) } } - override fun updateCards( - userId: AppUserId, - cardIds: Iterable, - update: (CardEntity) -> CardEntity - ): CardsDbResponse { + override fun updateCard(cardEntity: DbCard): DbCard { + validateCardEntityForUpdate(cardEntity) return connection.execute { - val timestamp = systemNow() - val ids = cardIds.map { it.asLong() } - val dbCards = PgDbCard.find { Cards.id inList ids }.associateBy { it.id.value } - val errors = mutableListOf() - ids.filterNot { it in dbCards.keys }.forEach { - errors.add(noCardFoundDbError(operation = "updateCards", id = it.asCardId())) - } - val dbDictionaries = mutableMapOf() - dbCards.forEach { - val dictionary = dbDictionaries.computeIfAbsent(it.value.dictionaryId.value) { k -> - checkNotNull(PgDbDictionary.findById(k)) - } - if (dictionary.userId.value != userId.asLong()) { - errors.add(forbiddenEntityDbError("updateCards", it.key.asCardId(), userId)) - } - } - if (errors.isNotEmpty()) { - return@execute CardsDbResponse(errors = errors) + val found = PgDbCard.findById(cardEntity.cardId.toCardsId()) + ?: throw DbDataException("Can't find card id = ${cardEntity.cardId}") + if (found.dictionaryId.value != cardEntity.dictionaryId.toLong()) { + throw DbDataException("Changing dictionary-id is not allowed; card id = ${cardEntity.cardId}") } - val cards = dbCards.values.onEach { - val new = update(it.toCardEntity()) - writeCardEntityToPgDbCard(from = new, to = it, timestamp = timestamp) - }.map { - it.toCardEntity() - } - val dictionaries = dbDictionaries.values.map { it.toDictionaryEntity() } - CardsDbResponse( - cards = cards, - dictionaries = dictionaries, - errors = emptyList(), - ) + val timestamp = systemNow() + writeCardEntityToPgDbCard(from = cardEntity, to = found, timestamp = timestamp) + found.toCardEntity() } } - override fun resetCard(userId: AppUserId, cardId: CardId): CardDbResponse { - return connection.execute { - val timestamp = systemNow() - val found = PgDbCard.findById(cardId.asLong()) ?: return@execute CardDbResponse( - noCardFoundDbError("resetCard", cardId) - ) - val errors = mutableListOf() - checkDictionaryUser("resetCard", userId, found.dictionaryId.asDictionaryId(), cardId, errors) - if (errors.isNotEmpty()) { - return@execute CardDbResponse(errors = errors) - } - writeCardEntityToPgDbCard(from = found.toCardEntity().copy(answered = 0), to = found, timestamp = timestamp) - return@execute CardDbResponse(card = found.toCardEntity()) + override fun updateCards(cardEntities: Iterable): List = connection.execute { + val res = mutableListOf() + val timestamp = systemNow() + BatchUpdateStatement(Cards).apply { + cardEntities.onEach { + validateCardEntityForUpdate(it) + addBatch(it.cardId.toCardsId()) + this[Cards.dictionaryId] = it.dictionaryId.toDictionariesId() + this[Cards.words] = it.toPgDbCardWordsJson() + this[Cards.answered] = it.answered + this[Cards.details] = it.detailsAsCommonCardDetailsDto().toJsonString() + this[Cards.changedAt] = timestamp + }.forEach { + res.add(it.copy(changedAt = timestamp.asKotlin())) + } + execute(TransactionManager.current()) } + res } - override fun removeCard(userId: AppUserId, cardId: CardId): RemoveCardDbResponse { + override fun deleteCard(cardId: String): DbCard { return connection.execute { - val card = PgDbCard.findById(cardId.asLong())?.toCardEntity() ?: return@execute RemoveCardDbResponse( - noCardFoundDbError("removeCard", cardId) - ) - val errors = mutableListOf() - checkDictionaryUser("removeCard", userId, card.dictionaryId, cardId, errors) - if (errors.isNotEmpty()) { - return@execute RemoveCardDbResponse(errors = errors) - } - if (Cards.deleteWhere { this.id eq cardId.asLong() } == 0) { - return@execute RemoveCardDbResponse(noCardFoundDbError("removeCard", cardId)) + val timestamp = systemNow() + val card = PgDbCard.findById(cardId.toCardsId())?.toCardEntity() + ?: throw DbDataException("Can't find card, id = $cardId") + if (Cards.deleteWhere { this.id eq cardId.toLong() } == 0) { + throw DbDataException("Can't delete card, id = $cardId") } - RemoveCardDbResponse(card = card) - } - } - - @Suppress("DuplicatedCode") - private fun checkDictionaryUser( - operation: String, - userId: AppUserId, - dictionaryId: DictionaryId, - entityId: Id, - errors: MutableList - ): PgDbDictionary? { - val dictionary = PgDbDictionary.findById(dictionaryId.asLong()) - if (dictionary == null) { - errors.add(noDictionaryFoundDbError(operation, dictionaryId)) - return null - } - if (dictionary.userId.value == userId.asLong()) { - return dictionary + card.copy(changedAt = timestamp.asKotlin()) } - errors.add(forbiddenEntityDbError(operation, entityId, userId)) - return null } } \ No newline at end of file diff --git a/db-pg/src/main/kotlin/PgDbDictionaryRepository.kt b/db-pg/src/main/kotlin/PgDbDictionaryRepository.kt index 47cd33f9..af532cd4 100644 --- a/db-pg/src/main/kotlin/PgDbDictionaryRepository.kt +++ b/db-pg/src/main/kotlin/PgDbDictionaryRepository.kt @@ -1,42 +1,20 @@ package com.gitlab.sszuev.flashcards.dbpg -import com.gitlab.sszuev.flashcards.common.SysConfig -import com.gitlab.sszuev.flashcards.common.asLong -import com.gitlab.sszuev.flashcards.common.documents.DocumentCard -import com.gitlab.sszuev.flashcards.common.documents.DocumentDictionary -import com.gitlab.sszuev.flashcards.common.documents.createReader -import com.gitlab.sszuev.flashcards.common.documents.createWriter -import com.gitlab.sszuev.flashcards.common.forbiddenEntityDbError -import com.gitlab.sszuev.flashcards.common.noDictionaryFoundDbError -import com.gitlab.sszuev.flashcards.common.parseCardWordsJson -import com.gitlab.sszuev.flashcards.common.status -import com.gitlab.sszuev.flashcards.common.systemNow -import com.gitlab.sszuev.flashcards.common.toDocumentExamples -import com.gitlab.sszuev.flashcards.common.toDocumentTranslations -import com.gitlab.sszuev.flashcards.common.wrongResourceDbError import com.gitlab.sszuev.flashcards.dbpg.dao.Cards import com.gitlab.sszuev.flashcards.dbpg.dao.Dictionaries -import com.gitlab.sszuev.flashcards.dbpg.dao.PgDbCard import com.gitlab.sszuev.flashcards.dbpg.dao.PgDbDictionary -import com.gitlab.sszuev.flashcards.model.common.AppError -import com.gitlab.sszuev.flashcards.model.common.AppUserId -import com.gitlab.sszuev.flashcards.model.domain.DictionaryEntity -import com.gitlab.sszuev.flashcards.model.domain.DictionaryId -import com.gitlab.sszuev.flashcards.model.domain.ResourceEntity +import com.gitlab.sszuev.flashcards.repositories.DbDataException +import com.gitlab.sszuev.flashcards.repositories.DbDictionary import com.gitlab.sszuev.flashcards.repositories.DbDictionaryRepository -import com.gitlab.sszuev.flashcards.repositories.DictionariesDbResponse -import com.gitlab.sszuev.flashcards.repositories.DictionaryDbResponse -import com.gitlab.sszuev.flashcards.repositories.ImportDictionaryDbResponse -import com.gitlab.sszuev.flashcards.repositories.RemoveDictionaryDbResponse +import com.gitlab.sszuev.flashcards.systemNow import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.insertAndGetId -import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.selectAll class PgDbDictionaryRepository( dbConfig: PgDbConfig = PgDbConfig(), - private val sysConfig: SysConfig = SysConfig(), ) : DbDictionaryRepository { private val connection by lazy { // lazy, to avoid initialization error when there is no real pg-database @@ -44,142 +22,45 @@ class PgDbDictionaryRepository( PgDbConnector.connection(dbConfig) } - override fun getAllDictionaries(userId: AppUserId): DictionariesDbResponse { - return connection.execute { - val dictionaries = - PgDbDictionary.find(Dictionaries.userId eq userId.asRecordId()).map { it.toDictionaryEntity() } - DictionariesDbResponse(dictionaries = dictionaries) - } + override fun findDictionaryById(dictionaryId: String): DbDictionary? = connection.execute { + PgDbDictionary.findById(dictionaryId.toLong())?.toDbDictionary() } - override fun createDictionary(userId: AppUserId, entity: DictionaryEntity): DictionaryDbResponse { - return connection.execute { - val timestamp = systemNow() - val dictionaryId = Dictionaries.insertAndGetId { - it[sourceLanguage] = entity.sourceLang.langId.asString() - it[targetLanguage] = entity.targetLang.langId.asString() - it[name] = entity.name - it[Dictionaries.userId] = userId.asLong() - it[changedAt] = timestamp - } - DictionaryDbResponse(dictionary = entity.copy(dictionaryId = dictionaryId.asDictionaryId())) - } - } - - override fun removeDictionary(userId: AppUserId, dictionaryId: DictionaryId): RemoveDictionaryDbResponse { - return connection.execute { - val errors = mutableListOf() - val found = checkDictionaryUser("removeDictionary", userId, dictionaryId, errors) - if (errors.isNotEmpty()) { - return@execute RemoveDictionaryDbResponse(errors = errors) - } - checkNotNull(found) - val cardIds = Cards.select { - Cards.dictionaryId eq found.id - }.map { - it[Cards.id] - } - Cards.deleteWhere { - this.id inList cardIds - } - val res = Dictionaries.deleteWhere { - Dictionaries.id eq found.id - } - RemoveDictionaryDbResponse( - errors = if (res == 0) - listOf(noDictionaryFoundDbError(operation = "removeDictionary", id = dictionaryId)) - else emptyList() - ) - } + override fun findDictionariesByUserId(userId: String): Sequence = connection.execute { + PgDbDictionary.find(Dictionaries.userId eq userId).map { it.toDbDictionary() }.asSequence() } - override fun importDictionary(userId: AppUserId, dictionaryId: DictionaryId): ImportDictionaryDbResponse { - return connection.execute { - val errors = mutableListOf() - val found = checkDictionaryUser("importDictionary", userId, dictionaryId, errors) - if (errors.isNotEmpty()) { - return@execute ImportDictionaryDbResponse(errors = errors) - } - checkNotNull(found) - val cards = PgDbCard.find { - Cards.dictionaryId eq found.id - } - val res = DocumentDictionary( - name = found.name, - sourceLang = found.sourceLang, - targetLang = found.targetLang, - cards = cards.map { card -> - val word = parseCardWordsJson(card.words).first() - DocumentCard( - text = word.word, - transcription = word.transcription, - partOfSpeech = word.partOfSpeech, - translations = word.toDocumentTranslations(), - examples = word.toDocumentExamples(), - status = sysConfig.status(card.answered), - ) - } - ) - val data = try { - createWriter().write(res) - } catch (ex: Exception) { - return@execute ImportDictionaryDbResponse(wrongResourceDbError(ex)) - } - ImportDictionaryDbResponse(resource = ResourceEntity(dictionaryId, data)) + override fun createDictionary(entity: DbDictionary): DbDictionary = connection.execute { + val timestamp = systemNow() + val dictionaryId = Dictionaries.insertAndGetId { + it[sourceLanguage] = entity.sourceLang.langId + it[targetLanguage] = entity.targetLang.langId + it[name] = entity.name + it[userId] = entity.userId + it[changedAt] = timestamp } + entity.copy(dictionaryId = dictionaryId.value.toString()) } - override fun exportDictionary(userId: AppUserId, resource: ResourceEntity): DictionaryDbResponse { - val timestamp = systemNow() - val document = try { - createReader().parse(resource.data) - } catch (ex: Exception) { - return DictionaryDbResponse(wrongResourceDbError(ex)) + override fun deleteDictionary(dictionaryId: String): DbDictionary = connection.execute { + require(dictionaryId.isNotBlank()) + val id = dictionaryId.toLong() + val found = PgDbDictionary.findById(id)?.toDbDictionary() ?: throw DbDataException("Can't find dictionary $id") + val cardIds = Cards.selectAll().where { + Cards.dictionaryId eq id + }.map { + it[Cards.id] } - return connection.execute { - val sourceLang = document.sourceLang - val targetLang = document.targetLang - val dictionaryId = Dictionaries.insertAndGetId { - it[sourceLanguage] = sourceLang - it[targetLanguage] = targetLang - it[name] = document.name - it[Dictionaries.userId] = userId.asLong() - it[changedAt] = timestamp - } - document.cards.forEach { - PgDbCard.new { - this.dictionaryId = dictionaryId - this.words = it.toPgDbCardWordsJson() - this.details = "{}" - this.changedAt = timestamp - } - } - val res = DictionaryEntity( - dictionaryId = dictionaryId.asDictionaryId(), - name = document.name, - sourceLang = createLangEntity(sourceLang), - targetLang = createLangEntity(targetLang), - ) - DictionaryDbResponse(dictionary = res) + Cards.deleteWhere { + this.id inList cardIds } - } - - @Suppress("DuplicatedCode") - private fun checkDictionaryUser( - operation: String, - userId: AppUserId, - dictionaryId: DictionaryId, - errors: MutableList - ): PgDbDictionary? { - val dictionary = PgDbDictionary.findById(dictionaryId.asLong()) - if (dictionary == null) { - errors.add(noDictionaryFoundDbError(operation, dictionaryId)) - return null + val res = Dictionaries.deleteWhere { + Dictionaries.id eq id } - if (dictionary.userId.value == userId.asLong()) { - return dictionary + if (res != 1) { + throw DbDataException("Can't delete dictionary $id") } - errors.add(forbiddenEntityDbError(operation, dictionaryId, userId)) - return null + found } + } \ No newline at end of file diff --git a/db-pg/src/main/kotlin/PgDbEntityMapper.kt b/db-pg/src/main/kotlin/PgDbEntityMapper.kt index 270ca0fa..474074f1 100644 --- a/db-pg/src/main/kotlin/PgDbEntityMapper.kt +++ b/db-pg/src/main/kotlin/PgDbEntityMapper.kt @@ -1,57 +1,39 @@ package com.gitlab.sszuev.flashcards.dbpg -import com.gitlab.sszuev.flashcards.common.LanguageRepository -import com.gitlab.sszuev.flashcards.common.asKotlin -import com.gitlab.sszuev.flashcards.common.asLong +import com.gitlab.sszuev.flashcards.asKotlin import com.gitlab.sszuev.flashcards.common.detailsAsCommonCardDetailsDto -import com.gitlab.sszuev.flashcards.common.documents.DocumentCard import com.gitlab.sszuev.flashcards.common.parseCardDetailsJson import com.gitlab.sszuev.flashcards.common.parseCardWordsJson import com.gitlab.sszuev.flashcards.common.toCardEntityDetails import com.gitlab.sszuev.flashcards.common.toCardEntityStats import com.gitlab.sszuev.flashcards.common.toCardWordEntity -import com.gitlab.sszuev.flashcards.common.toCommonCardDtoDetails -import com.gitlab.sszuev.flashcards.common.toCommonWordDtoList import com.gitlab.sszuev.flashcards.common.toJsonString import com.gitlab.sszuev.flashcards.common.wordsAsCommonWordDtoList import com.gitlab.sszuev.flashcards.dbpg.dao.Cards import com.gitlab.sszuev.flashcards.dbpg.dao.Dictionaries import com.gitlab.sszuev.flashcards.dbpg.dao.PgDbCard import com.gitlab.sszuev.flashcards.dbpg.dao.PgDbDictionary -import com.gitlab.sszuev.flashcards.dbpg.dao.PgDbUser -import com.gitlab.sszuev.flashcards.dbpg.dao.Users -import com.gitlab.sszuev.flashcards.model.common.AppAuthId -import com.gitlab.sszuev.flashcards.model.common.AppUserEntity -import com.gitlab.sszuev.flashcards.model.common.AppUserId -import com.gitlab.sszuev.flashcards.model.domain.CardEntity -import com.gitlab.sszuev.flashcards.model.domain.CardId -import com.gitlab.sszuev.flashcards.model.domain.DictionaryEntity -import com.gitlab.sszuev.flashcards.model.domain.DictionaryId -import com.gitlab.sszuev.flashcards.model.domain.LangEntity -import com.gitlab.sszuev.flashcards.model.domain.LangId -import com.gitlab.sszuev.flashcards.model.domain.Stage +import com.gitlab.sszuev.flashcards.repositories.DbCard +import com.gitlab.sszuev.flashcards.repositories.DbDictionary +import com.gitlab.sszuev.flashcards.repositories.DbLang +import com.gitlab.sszuev.flashcards.repositories.LanguageRepository import org.jetbrains.exposed.dao.id.EntityID import java.time.LocalDateTime -import java.util.UUID -internal fun PgDbUser.toAppUserEntity(): AppUserEntity = AppUserEntity( - id = this.id.asUserId(), - authId = this.uuid.asAppAuthId(), -) - -internal fun PgDbDictionary.toDictionaryEntity(): DictionaryEntity = DictionaryEntity( - dictionaryId = this.id.asDictionaryId(), +internal fun PgDbDictionary.toDbDictionary(): DbDictionary = DbDictionary( + dictionaryId = this.id.value.toString(), + userId = this.userId, name = this.name, - sourceLang = createLangEntity(this.sourceLang), - targetLang = createLangEntity(this.targetLang), + sourceLang = createDbLang(this.sourceLang), + targetLang = createDbLang(this.targetLang), ) -internal fun PgDbCard.toCardEntity(): CardEntity { +internal fun PgDbCard.toCardEntity(): DbCard { val words = parseCardWordsJson(this.words) val details = parseCardDetailsJson(this.details) - return CardEntity( - cardId = this.id.asCardId(), - dictionaryId = this.dictionaryId.asDictionaryId(), + return DbCard( + cardId = this.id.value.toString(), + dictionaryId = this.dictionaryId.value.toString(), words = words.map { it.toCardWordEntity() }, details = details.toCardEntityDetails(), stats = details.toCardEntityStats(), @@ -60,39 +42,21 @@ internal fun PgDbCard.toCardEntity(): CardEntity { ) } -internal fun writeCardEntityToPgDbCard(from: CardEntity, to: PgDbCard, timestamp: LocalDateTime) { - to.dictionaryId = from.dictionaryId.asRecordId() +internal fun writeCardEntityToPgDbCard(from: DbCard, to: PgDbCard, timestamp: LocalDateTime) { + to.dictionaryId = from.dictionaryId.toDictionariesId() to.words = from.toPgDbCardWordsJson() to.answered = from.answered to.details = from.detailsAsCommonCardDetailsDto().toJsonString() to.changedAt = timestamp } -internal fun Map.toPgDbCardDetailsJson(): String = toCommonCardDtoDetails().toJsonString() - -internal fun CardEntity.toPgDbCardWordsJson(): String = wordsAsCommonWordDtoList().toJsonString() - -internal fun DocumentCard.toPgDbCardWordsJson(): String = toCommonWordDtoList().toJsonString() - -internal fun EntityID.asUserId(): AppUserId = AppUserId(value.toString()) - -internal fun EntityID.asDictionaryId(): DictionaryId = value.asDictionaryId() +internal fun DbCard.toPgDbCardWordsJson(): String = wordsAsCommonWordDtoList().toJsonString() -internal fun EntityID.asCardId(): CardId = value.asCardId() +internal fun String.toDictionariesId(): EntityID = EntityID(toLong(), Dictionaries) -internal fun AppUserId.asRecordId(): EntityID = EntityID(asLong(), Users) +internal fun String.toCardsId(): EntityID = EntityID(toLong(), Cards) -internal fun DictionaryId.asRecordId(): EntityID = EntityID(asLong(), Dictionaries) - -internal fun CardId.asRecordId(): EntityID = EntityID(asLong(), Cards) - -internal fun createLangEntity(tag: String) = LangEntity( - langId = LangId(tag), +internal fun createDbLang(tag: String) = DbLang( + langId = tag, partsOfSpeech = LanguageRepository.partsOfSpeech(tag) -) - -private fun UUID.asAppAuthId(): AppAuthId = AppAuthId(toString()) - -internal fun Long.asDictionaryId(): DictionaryId = DictionaryId(toString()) - -internal fun Long.asCardId(): CardId = CardId(toString()) \ No newline at end of file +) \ No newline at end of file diff --git a/db-pg/src/main/kotlin/PgDbUserRepository.kt b/db-pg/src/main/kotlin/PgDbUserRepository.kt deleted file mode 100644 index 97bd407a..00000000 --- a/db-pg/src/main/kotlin/PgDbUserRepository.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.gitlab.sszuev.flashcards.dbpg - -import com.github.benmanes.caffeine.cache.Cache -import com.github.benmanes.caffeine.cache.Caffeine -import com.gitlab.sszuev.flashcards.common.noUserFoundDbError -import com.gitlab.sszuev.flashcards.common.wrongUserUuidDbError -import com.gitlab.sszuev.flashcards.dbpg.dao.PgDbUser -import com.gitlab.sszuev.flashcards.dbpg.dao.Users -import com.gitlab.sszuev.flashcards.model.common.AppAuthId -import com.gitlab.sszuev.flashcards.model.common.AppUserEntity -import com.gitlab.sszuev.flashcards.repositories.DbUserRepository -import com.gitlab.sszuev.flashcards.repositories.UserEntityDbResponse -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import java.util.UUID - -class PgDbUserRepository( - dbConfig: PgDbConfig = PgDbConfig(), -) : DbUserRepository { - private val connection by lazy { - // lazy, to avoid initialization error when there is no real pg-database - // and memory-storage is used instead - PgDbConnector.connection(dbConfig) - } - private val cache: Cache = Caffeine.newBuilder().build() - - override fun getUser(authId: AppAuthId): UserEntityDbResponse { - val uuid = try { - UUID.fromString(authId.asString()) - } catch (ex: IllegalArgumentException) { - return UserEntityDbResponse( - user = AppUserEntity.EMPTY, errors = listOf(wrongUserUuidDbError("getUser", authId)) - ) - } - return connection.execute { - // use local cache - // expected that deletion when using different devices is rare - // also, currently user has no details - val entity = cache.getIfPresent(uuid) ?: PgDbUser.find(Users.uuid eq uuid).singleOrNull()?.toAppUserEntity() - if (entity == null) { - UserEntityDbResponse( - user = AppUserEntity.EMPTY, errors = listOf(noUserFoundDbError("getUser", authId)) - ) - } else { - cache.put(uuid, entity) - UserEntityDbResponse(user = entity) - } - } - } -} \ No newline at end of file diff --git a/db-pg/src/main/kotlin/dao/PgDbDictionary.kt b/db-pg/src/main/kotlin/dao/PgDbDictionary.kt index 519430d4..8e1eb857 100644 --- a/db-pg/src/main/kotlin/dao/PgDbDictionary.kt +++ b/db-pg/src/main/kotlin/dao/PgDbDictionary.kt @@ -3,14 +3,15 @@ package com.gitlab.sszuev.flashcards.dbpg.dao import org.jetbrains.exposed.dao.Entity import org.jetbrains.exposed.dao.EntityClass import org.jetbrains.exposed.dao.id.EntityID +import java.time.LocalDateTime class PgDbDictionary(id: EntityID) : Entity(id) { companion object : EntityClass(Dictionaries) - var userId by Dictionaries.userId - var name by Dictionaries.name - val sourceLang by Dictionaries.sourceLanguage - val targetLang by Dictionaries.targetLanguage - var details by Dictionaries.details - var changedAt by Dictionaries.changedAt + var userId: String by Dictionaries.userId + var name: String by Dictionaries.name + val sourceLang: String by Dictionaries.sourceLanguage + val targetLang: String by Dictionaries.targetLanguage + var details: String by Dictionaries.details + var changedAt: LocalDateTime by Dictionaries.changedAt } \ No newline at end of file diff --git a/db-pg/src/main/kotlin/dao/PgDbUser.kt b/db-pg/src/main/kotlin/dao/PgDbUser.kt deleted file mode 100644 index bc0355e8..00000000 --- a/db-pg/src/main/kotlin/dao/PgDbUser.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.gitlab.sszuev.flashcards.dbpg.dao - -import org.jetbrains.exposed.dao.Entity -import org.jetbrains.exposed.dao.EntityClass -import org.jetbrains.exposed.dao.id.EntityID - -class PgDbUser(id: EntityID) : Entity(id) { - companion object : EntityClass(Users) - - var uuid by Users.uuid - var details by Users.details - var changedAt by Users.changedAt -} \ No newline at end of file diff --git a/db-pg/src/main/kotlin/dao/Tables.kt b/db-pg/src/main/kotlin/dao/Tables.kt index 17466afd..3f00ad8f 100644 --- a/db-pg/src/main/kotlin/dao/Tables.kt +++ b/db-pg/src/main/kotlin/dao/Tables.kt @@ -8,6 +8,7 @@ import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.javatime.datetime import org.postgresql.util.PGobject import java.sql.ResultSet +import java.time.LocalDateTime /** @@ -18,12 +19,12 @@ object Dictionaries : LongIdTableWithSequence( idSeqName = "dictionaries_id_seq", pkeyName = "dictionaries_pkey" ) { - val name = varchar("name", 1024) - val userId = reference("user_id", Users.id) - val sourceLanguage = varchar("source_lang", 42) - val targetLanguage = varchar("target_lang", 42) - val details = json("details") - val changedAt = datetime("changed_at") + val name: Column = varchar("name", 1024) + val userId: Column = varchar("user_id", 36) + val sourceLanguage: Column = varchar("source_lang", 42) + val targetLanguage: Column = varchar("target_lang", 42) + val details: Column = json("details") + val changedAt: Column = datetime("changed_at") } /** @@ -37,15 +38,6 @@ object Cards : LongIdTableWithSequence(tableName = "cards", idSeqName = "cards_i val changedAt = datetime("changed_at") } -/** - * id;uuid,role - */ -object Users : LongIdTableWithSequence(tableName = "users", idSeqName = "users_id_seq", pkeyName = "users_pkey") { - val uuid = uuid("uuid").uniqueIndex() - val details = json("details") - val changedAt = datetime("changed_at") -} - open class LongIdTableWithSequence(tableName: String, idSeqName: String, pkeyName: String, columnName: String = "id") : IdTable(tableName) { final override val id: Column> = long(columnName).autoIncrement(idSeqName).entityId() diff --git a/db-pg/src/main/resources/migrations/schema/001-init-schema.sql b/db-pg/src/main/resources/migrations/schema/001-init-schema.sql index a21b9296..b0263d9b 100644 --- a/db-pg/src/main/resources/migrations/schema/001-init-schema.sql +++ b/db-pg/src/main/resources/migrations/schema/001-init-schema.sql @@ -1,14 +1,7 @@ -CREATE TABLE public.users ( - id bigint NOT NULL, - uuid UUID NOT NULL UNIQUE, - details JSON NOT NULL DEFAULT '{}'::JSON, - changed_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP -); - CREATE TABLE public.dictionaries ( id BIGINT NOT NULL, name VARCHAR(1024) NOT NULL, - user_id bigint NOT NULL, + user_id VARCHAR(36) NOT NULL, target_lang VARCHAR(42) NOT NULL, source_lang VARCHAR(42) NOT NULL, details JSON NOT NULL DEFAULT '{}'::JSON, @@ -24,15 +17,6 @@ CREATE TABLE public.cards ( changed_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP ); -ALTER TABLE public.users ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY ( - SEQUENCE NAME public.users_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1 -); - ALTER TABLE public.dictionaries ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY ( SEQUENCE NAME public.dictionaries_id_seq START WITH 1 @@ -57,11 +41,5 @@ ALTER TABLE ONLY public.cards ALTER TABLE ONLY public.dictionaries ADD CONSTRAINT dictionaries_pkey PRIMARY KEY (id); -ALTER TABLE ONLY public.users - ADD CONSTRAINT users_pkey PRIMARY KEY (id); - -ALTER TABLE ONLY public.dictionaries - ADD CONSTRAINT fk_dictionaries_users_id FOREIGN KEY (user_id) REFERENCES public.users(id); - ALTER TABLE ONLY public.cards ADD CONSTRAINT fk_cards_dictionaries_id FOREIGN KEY (dictionary_id) REFERENCES public.dictionaries(id); \ No newline at end of file diff --git a/db-pg/src/test/kotlin/PgDbUserRepositoryTest.kt b/db-pg/src/test/kotlin/PgDbUserRepositoryTest.kt deleted file mode 100644 index b5b5ac69..00000000 --- a/db-pg/src/test/kotlin/PgDbUserRepositoryTest.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.gitlab.sszuev.flashcards.dbpg - -import com.gitlab.sszuev.flashcards.dbcommon.DbUserRepositoryTest -import org.junit.jupiter.api.Order - -@Order(1) -internal class PgDbUserRepositoryTest : DbUserRepositoryTest() { - override val repository = PgDbUserRepository(PgTestContainer.config) -} \ No newline at end of file diff --git a/db-pg/src/test/resources/migrations/data/001-01-before-load-test-data.sql b/db-pg/src/test/resources/migrations/data/001-01-before-load-test-data.sql index e15aeeee..6067b4bb 100644 --- a/db-pg/src/test/resources/migrations/data/001-01-before-load-test-data.sql +++ b/db-pg/src/test/resources/migrations/data/001-01-before-load-test-data.sql @@ -1,4 +1,3 @@ -ALTER TABLE public.users ALTER COLUMN details TYPE VARCHAR; ALTER TABLE public.dictionaries ALTER COLUMN details TYPE VARCHAR; ALTER TABLE public.cards ALTER COLUMN details TYPE VARCHAR; ALTER TABLE public.cards ALTER COLUMN words TYPE VARCHAR; \ No newline at end of file diff --git a/db-pg/src/test/resources/migrations/data/001-02-test-dictionaries.csv b/db-pg/src/test/resources/migrations/data/001-02-test-dictionaries.csv index 3b73692b..466e7a37 100644 --- a/db-pg/src/test/resources/migrations/data/001-02-test-dictionaries.csv +++ b/db-pg/src/test/resources/migrations/data/001-02-test-dictionaries.csv @@ -1,3 +1,3 @@ id,name,user_id,source_lang,target_lang,details,changed_at -1,Irregular Verbs,42,en,ru,{},2022-12-26T16:04:14 -2,Weather,42,en,ru,{},2022-12-26T16:04:14 \ No newline at end of file +1,Irregular Verbs,c9a414f5-3f75-4494-b664-f4c8b33ff4e6,en,ru,{},2022-12-26T16:04:14 +2,Weather,c9a414f5-3f75-4494-b664-f4c8b33ff4e6,en,ru,{},2022-12-26T16:04:14 \ No newline at end of file diff --git a/db-pg/src/test/resources/migrations/data/001-02-test-users.csv b/db-pg/src/test/resources/migrations/data/001-02-test-users.csv deleted file mode 100644 index 21c35afe..00000000 --- a/db-pg/src/test/resources/migrations/data/001-02-test-users.csv +++ /dev/null @@ -1,2 +0,0 @@ -id,uuid,details,changed_at -42,c9a414f5-3f75-4494-b664-f4c8b33ff4e6,{},2022-12-26T16:04:14 \ No newline at end of file diff --git a/db-pg/src/test/resources/migrations/data/001-03-test-data-sequences.sql b/db-pg/src/test/resources/migrations/data/001-03-test-data-sequences.sql index 8c193d1b..f0e161ab 100644 --- a/db-pg/src/test/resources/migrations/data/001-03-test-data-sequences.sql +++ b/db-pg/src/test/resources/migrations/data/001-03-test-data-sequences.sql @@ -1,4 +1,2 @@ -SELECT setval('public.users_id_seq', 1001, true); SELECT setval('public.cards_id_seq', 10001, true); -SELECT setval('public.dictionaries_id_seq', 101, true); -SELECT setval('public.users_id_seq', 1001, true); \ No newline at end of file +SELECT setval('public.dictionaries_id_seq', 101, true); \ No newline at end of file diff --git a/db-pg/src/test/resources/migrations/data/001-04-after-load-test-data.sql b/db-pg/src/test/resources/migrations/data/001-04-after-load-test-data.sql index b101eabe..992112a6 100644 --- a/db-pg/src/test/resources/migrations/data/001-04-after-load-test-data.sql +++ b/db-pg/src/test/resources/migrations/data/001-04-after-load-test-data.sql @@ -1,4 +1,3 @@ -ALTER TABLE public.users ALTER COLUMN details TYPE JSON USING details::json; ALTER TABLE public.dictionaries ALTER COLUMN details TYPE JSON USING details::json; ALTER TABLE public.cards ALTER COLUMN details TYPE JSON USING details::jsonb; ALTER TABLE public.cards ALTER COLUMN words TYPE JSON USING words::json; \ No newline at end of file diff --git a/db-pg/src/test/resources/migrations/data/test-data-changelog.xml b/db-pg/src/test/resources/migrations/data/test-data-changelog.xml index c84f1dba..22527721 100644 --- a/db-pg/src/test/resources/migrations/data/test-data-changelog.xml +++ b/db-pg/src/test/resources/migrations/data/test-data-changelog.xml @@ -11,14 +11,6 @@ - - IntRange(1, 42).map { cardId -> dictionaryId to cardId } } .map { + val word = "XXX-${it.first}-${it.second}" stubCard.copy( cardId = CardId(it.second.toString()), dictionaryId = DictionaryId(it.first.toString()), words = listOf( CardWordEntity( - word = "XXX-${it.first}-${it.second}" + word = word, + sound = TTSResourceId("sl:$word"), ), ), + sound = TTSResourceId("sl:$word"), ) }