diff --git a/gradle.properties b/gradle.properties index 3d1b14d..8ed652a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -49,3 +49,6 @@ exposedVersion=0.41.1 # Kafka kafkaVersion=3.4.0 + +# Cassandra +cassandraDriverVersion=4.13.0 \ No newline at end of file diff --git a/ok-marketplace-repo-cassandra/build.gradle.kts b/ok-marketplace-repo-cassandra/build.gradle.kts new file mode 100644 index 0000000..c189cdf --- /dev/null +++ b/ok-marketplace-repo-cassandra/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + kotlin("jvm") + kotlin("kapt") +} + +dependencies { + val coroutinesVersion: String by project + val cassandraDriverVersion: String by project + val testContainersVersion: String by project + val logbackVersion: String by project + val kotlinLoggingJvmVersion: String by project + val kmpUUIDVersion: String by project + + implementation(project(":ok-marketplace-common")) + + implementation(kotlin("stdlib")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion") + + implementation("com.benasher44:uuid:$kmpUUIDVersion") + + implementation("com.datastax.oss:java-driver-core:$cassandraDriverVersion") + implementation("com.datastax.oss:java-driver-query-builder:$cassandraDriverVersion") + kapt("com.datastax.oss:java-driver-mapper-processor:$cassandraDriverVersion") + implementation("com.datastax.oss:java-driver-mapper-runtime:$cassandraDriverVersion") + + // log + implementation("ch.qos.logback:logback-classic:$logbackVersion") + implementation("io.github.microutils:kotlin-logging-jvm:$kotlinLoggingJvmVersion") + + testImplementation(project(":ok-marketplace-repo-tests")) + testImplementation("org.testcontainers:cassandra:$testContainersVersion") +} diff --git a/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/AdCassandraDAO.kt b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/AdCassandraDAO.kt new file mode 100644 index 0000000..e0ac182 --- /dev/null +++ b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/AdCassandraDAO.kt @@ -0,0 +1,29 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.cassandra + +import com.datastax.oss.driver.api.mapper.annotations.Dao +import com.datastax.oss.driver.api.mapper.annotations.Delete +import com.datastax.oss.driver.api.mapper.annotations.Insert +import com.datastax.oss.driver.api.mapper.annotations.QueryProvider +import com.datastax.oss.driver.api.mapper.annotations.Select +import com.datastax.oss.driver.api.mapper.annotations.Update +import ru.otus.otuskotlin.marketplace.backend.repo.cassandra.model.AdCassandraDTO +import ru.otus.otuskotlin.marketplace.common.repo.DbAdFilterRequest +import java.util.concurrent.CompletionStage + +@Dao +interface AdCassandraDAO { + @Insert + fun create(dto: AdCassandraDTO): CompletionStage + + @Select + fun read(id: String): CompletionStage + + @Update(customIfClause = "lock = :prevLock") + fun update(dto: AdCassandraDTO, prevLock: String): CompletionStage + + @Delete(customWhereClause = "id = :id", customIfClause = "lock = :prevLock", entityClass = [AdCassandraDTO::class]) + fun delete(id: String, prevLock: String): CompletionStage + + @QueryProvider(providerClass = AdCassandraSearchProvider::class, entityHelpers = [AdCassandraDTO::class]) + fun search(filter: DbAdFilterRequest): CompletionStage> +} diff --git a/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/AdCassandraSearchProvider.kt b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/AdCassandraSearchProvider.kt new file mode 100644 index 0000000..1f68658 --- /dev/null +++ b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/AdCassandraSearchProvider.kt @@ -0,0 +1,67 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.cassandra + +import com.datastax.oss.driver.api.core.cql.AsyncResultSet +import com.datastax.oss.driver.api.mapper.MapperContext +import com.datastax.oss.driver.api.mapper.entity.EntityHelper +import com.datastax.oss.driver.api.querybuilder.QueryBuilder +import ru.otus.otuskotlin.marketplace.backend.repo.cassandra.model.AdCassandraDTO +import ru.otus.otuskotlin.marketplace.backend.repo.cassandra.model.toTransport +import ru.otus.otuskotlin.marketplace.common.models.MkplDealSide +import ru.otus.otuskotlin.marketplace.common.models.MkplUserId +import ru.otus.otuskotlin.marketplace.common.repo.DbAdFilterRequest +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionStage +import java.util.function.BiConsumer + +class AdCassandraSearchProvider( + private val context: MapperContext, + private val entityHelper: EntityHelper +) { + fun search(filter: DbAdFilterRequest): CompletionStage> { + var select = entityHelper.selectStart().allowFiltering() + + if (filter.titleFilter.isNotBlank()) { + select = select + .whereColumn(AdCassandraDTO.COLUMN_TITLE) + .like(QueryBuilder.literal("%${filter.titleFilter}%")) + } + if (filter.ownerId != MkplUserId.NONE) { + select = select + .whereColumn(AdCassandraDTO.COLUMN_OWNER_ID) + .isEqualTo(QueryBuilder.literal(filter.ownerId.asString(), context.session.context.codecRegistry)) + } + if (filter.dealSide != MkplDealSide.NONE) { + select = select + .whereColumn(AdCassandraDTO.COLUMN_AD_TYPE) + .isEqualTo(QueryBuilder.literal(filter.dealSide.toTransport(), context.session.context.codecRegistry)) + } + + val asyncFetcher = AsyncFetcher() + + context.session + .executeAsync(select.build()) + .whenComplete(asyncFetcher) + + return asyncFetcher.stage + } + + inner class AsyncFetcher : BiConsumer { + private val buffer = mutableListOf() + private val future = CompletableFuture>() + val stage: CompletionStage> = future + + override fun accept(resultSet: AsyncResultSet?, t: Throwable?) { + when { + t != null -> future.completeExceptionally(t) + resultSet == null -> future.completeExceptionally(IllegalStateException("ResultSet should not be null")) + else -> { + buffer.addAll(resultSet.currentPage().map { entityHelper.get(it, false) }) + if (resultSet.hasMorePages()) + resultSet.fetchNextPage().whenComplete(this) + else + future.complete(buffer) + } + } + } + } +} \ No newline at end of file diff --git a/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/CassandraMapper.kt b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/CassandraMapper.kt new file mode 100644 index 0000000..5b7b2de --- /dev/null +++ b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/CassandraMapper.kt @@ -0,0 +1,17 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.cassandra + +import com.datastax.oss.driver.api.core.CqlSession +import com.datastax.oss.driver.api.mapper.annotations.DaoFactory +import com.datastax.oss.driver.api.mapper.annotations.DaoKeyspace +import com.datastax.oss.driver.api.mapper.annotations.DaoTable +import com.datastax.oss.driver.api.mapper.annotations.Mapper + +@Mapper +interface CassandraMapper { + @DaoFactory + fun adDao(@DaoKeyspace keyspace: String, @DaoTable tableName: String): AdCassandraDAO + + companion object { + fun builder(session: CqlSession) = CassandraMapperBuilder(session) + } +} \ No newline at end of file diff --git a/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/CompletionStageOfUnitProducer.kt b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/CompletionStageOfUnitProducer.kt new file mode 100644 index 0000000..5caf294 --- /dev/null +++ b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/CompletionStageOfUnitProducer.kt @@ -0,0 +1,35 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.cassandra + +import com.datastax.oss.driver.api.core.cql.Statement +import com.datastax.oss.driver.api.core.type.reflect.GenericType +import com.datastax.oss.driver.api.mapper.MapperContext +import com.datastax.oss.driver.api.mapper.entity.EntityHelper +import com.datastax.oss.driver.api.mapper.result.MapperResultProducer +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionStage + +class CompletionStageOfUnitProducer : MapperResultProducer { + companion object { + val PRODUCED_TYPE = object : GenericType>() {} + } + + override fun canProduce(resultType: GenericType<*>): Boolean = + resultType == PRODUCED_TYPE + + override fun execute( + statement: Statement<*>, + context: MapperContext, + entityHelper: EntityHelper<*>? + ): CompletionStage { + val result = CompletableFuture() + context.session.executeAsync(statement).whenComplete { _, e -> + if (e != null) result.completeExceptionally(e) + else result.complete(Unit) + } + return result + } + + override fun wrapError(e: Exception): CompletionStage = + CompletableFutures.failedFuture(e) +} \ No newline at end of file diff --git a/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/KotlinProducerService.kt b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/KotlinProducerService.kt new file mode 100644 index 0000000..badec13 --- /dev/null +++ b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/KotlinProducerService.kt @@ -0,0 +1,9 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.cassandra + +import com.datastax.oss.driver.api.mapper.result.MapperResultProducer +import com.datastax.oss.driver.api.mapper.result.MapperResultProducerService + +class KotlinProducerService : MapperResultProducerService { + override fun getProducers(): MutableIterable = + mutableListOf(CompletionStageOfUnitProducer()) +} \ No newline at end of file diff --git a/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/RepoAdCassandra.kt b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/RepoAdCassandra.kt new file mode 100644 index 0000000..7c7bc4d --- /dev/null +++ b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/RepoAdCassandra.kt @@ -0,0 +1,210 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.cassandra + +import com.benasher44.uuid.uuid4 +import com.datastax.oss.driver.api.core.CqlSession +import com.datastax.oss.driver.api.querybuilder.SchemaBuilder +import com.datastax.oss.driver.internal.core.type.codec.extras.enums.EnumNameCodec +import com.datastax.oss.driver.internal.core.type.codec.registry.DefaultCodecRegistry +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import org.slf4j.LoggerFactory +import ru.otus.otuskotlin.marketplace.backend.repo.cassandra.model.AdCassandraDTO +import ru.otus.otuskotlin.marketplace.backend.repo.cassandra.model.AdDealSide +import ru.otus.otuskotlin.marketplace.backend.repo.cassandra.model.AdVisibility +import ru.otus.otuskotlin.marketplace.common.helpers.asMkplError +import ru.otus.otuskotlin.marketplace.common.models.MkplAd +import ru.otus.otuskotlin.marketplace.common.models.MkplAdId +import ru.otus.otuskotlin.marketplace.common.models.MkplAdLock +import ru.otus.otuskotlin.marketplace.common.models.MkplError +import ru.otus.otuskotlin.marketplace.common.repo.* +import java.net.InetAddress +import java.net.InetSocketAddress +import java.util.concurrent.CompletionStage +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +class RepoAdCassandra( + private val keyspaceName: String, + private val host: String = "", + private val port: Int = 9042, + private val user: String = "cassandra", + private val pass: String = "cassandra", + private val testing: Boolean = false, + private val timeout: Duration = 30.toDuration(DurationUnit.SECONDS), + private val randomUuid: () -> String = { uuid4().toString() }, + initObjects: Collection = emptyList(), +) : IAdRepository { + private val log = LoggerFactory.getLogger(javaClass) + + private val codecRegistry by lazy { + DefaultCodecRegistry("default").apply { + register(EnumNameCodec(AdVisibility::class.java)) + register(EnumNameCodec(AdDealSide::class.java)) + } + } + + private val session by lazy { + CqlSession.builder() + .addContactPoints(parseAddresses(host, port)) + .withLocalDatacenter("datacenter1") + .withAuthCredentials(user, pass) + .withCodecRegistry(codecRegistry) + .build() + } + + private val mapper by lazy { CassandraMapper.builder(session).build() } + + private fun createSchema(keyspace: String) { + session.execute( + SchemaBuilder + .createKeyspace(keyspace) + .ifNotExists() + .withSimpleStrategy(1) + .build() + ) + session.execute(AdCassandraDTO.table(keyspace, AdCassandraDTO.TABLE_NAME)) + session.execute(AdCassandraDTO.titleIndex(keyspace, AdCassandraDTO.TABLE_NAME)) + } + + private val dao by lazy { + if (testing) { + createSchema(keyspaceName) + } + mapper.adDao(keyspaceName, AdCassandraDTO.TABLE_NAME).apply { + runBlocking { + initObjects.map { model -> + withTimeout(timeout) { + create(AdCassandraDTO(model)).await() + } + } + } + } + } + + private fun errorToAdResponse(e: Exception) = DbAdResponse.error(e.asMkplError()) + private fun errorToAdsResponse(e: Exception) = DbAdsResponse.error(e.asMkplError()) + + private suspend inline fun doDbAction( + name: String, + crossinline daoAction: () -> CompletionStage, + okToResponse: (DbRes) -> Response, + errorToResponse: (Exception) -> Response + ): Response = doDbAction( + name, + { + val dbRes = withTimeout(timeout) { daoAction().await() } + okToResponse(dbRes) + }, + errorToResponse + ) + + private suspend inline fun readAndDoDbAction( + name: String, + id: MkplAdId, + successResult: MkplAd?, + daoAction: () -> CompletionStage, + errorToResponse: (Exception) -> DbAdResponse + ): DbAdResponse = + if (id == MkplAdId.NONE) + ID_IS_EMPTY + else doDbAction( + name, + { + val read = dao.read(id.asString()).await() + if (read == null) ID_NOT_FOUND + else { + val success = daoAction().await() + if (success) DbAdResponse.success(successResult ?: read.toAdModel()) + else DbAdResponse( + read.toAdModel(), + false, + CONCURRENT_MODIFICATION.errors + ) + } + }, + errorToResponse + ) + + private inline fun doDbAction( + name: String, + daoAction: () -> Response, + errorToResponse: (Exception) -> Response + ): Response = + try { + daoAction() + } catch (e: Exception) { + log.error("Failed to $name", e) + errorToResponse(e) + } + + override suspend fun createAd(rq: DbAdRequest): DbAdResponse { + val new = rq.ad.copy(id = MkplAdId(randomUuid()), lock = MkplAdLock(randomUuid())) + return doDbAction( + "create", + { dao.create(AdCassandraDTO(new)) }, + { DbAdResponse.success(new) }, + ::errorToAdResponse + ) + } + + override suspend fun readAd(rq: DbAdIdRequest): DbAdResponse = + if (rq.id == MkplAdId.NONE) + ID_IS_EMPTY + else doDbAction( + "read", + { dao.read(rq.id.asString()) }, + { found -> + if (found != null) DbAdResponse.success(found.toAdModel()) + else ID_NOT_FOUND + }, + ::errorToAdResponse + ) + + override suspend fun updateAd(rq: DbAdRequest): DbAdResponse { + val prevLock = rq.ad.lock.asString() + val new = rq.ad.copy(lock = MkplAdLock(randomUuid())) + val dto = AdCassandraDTO(new) + + return readAndDoDbAction( + "update", + rq.ad.id, + new, + { dao.update(dto, prevLock) }, + ::errorToAdResponse + ) + } + + override suspend fun deleteAd(rq: DbAdIdRequest): DbAdResponse = + readAndDoDbAction( + "delete", + rq.id, + null, + { dao.delete(rq.id.asString(), rq.lock.asString()) }, + ::errorToAdResponse + ) + + + override suspend fun searchAd(rq: DbAdFilterRequest): DbAdsResponse = + doDbAction( + "search", + { dao.search(rq) }, + { found -> + DbAdsResponse.success(found.map { it.toAdModel() }) + }, + ::errorToAdsResponse + ) + + companion object { + private val ID_IS_EMPTY = DbAdResponse.error(MkplError(field = "id", message = "Id is empty")) + private val ID_NOT_FOUND = + DbAdResponse.error(MkplError(field = "id", code = "not-found", message = "Not Found")) + private val CONCURRENT_MODIFICATION = + DbAdResponse.error(MkplError(field = "lock", code = "concurrency", message = "Concurrent modification")) + } +} + +private fun parseAddresses(hosts: String, port: Int): Collection = hosts + .split(Regex("""\s*,\s*""")) + .map { InetSocketAddress(InetAddress.getByName(it), port) } diff --git a/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/model/AdCassandraDTO.kt b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/model/AdCassandraDTO.kt new file mode 100644 index 0000000..552dbd3 --- /dev/null +++ b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/model/AdCassandraDTO.kt @@ -0,0 +1,92 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.cassandra.model + +import com.datastax.oss.driver.api.core.type.DataTypes +import com.datastax.oss.driver.api.mapper.annotations.CqlName +import com.datastax.oss.driver.api.mapper.annotations.Entity +import com.datastax.oss.driver.api.mapper.annotations.PartitionKey +import com.datastax.oss.driver.api.querybuilder.SchemaBuilder +import ru.otus.otuskotlin.marketplace.common.models.* + +@Entity +data class AdCassandraDTO( + @field:CqlName(COLUMN_ID) + @field:PartitionKey // можно задать порядок + var id: String? = null, + @field:CqlName(COLUMN_TITLE) + var title: String? = null, + @field:CqlName(COLUMN_DESCRIPTION) + var description: String? = null, + @field:CqlName(COLUMN_OWNER_ID) + var ownerId: String? = null, + @field:CqlName(COLUMN_VISIBILITY) + var visibility: AdVisibility? = null, + @field:CqlName(COLUMN_PRODUCT) + var productId: String? = null, + // Нельзя использовать в моделях хранения внутренние модели. + // При изменении внутренних моделей, БД автоматически не изменится, + // а потому будет Runtime ошибка, которая вылезет только на продуктовом стенде + @field:CqlName(COLUMN_AD_TYPE) + var adType: AdDealSide? = null, + @field:CqlName(COLUMN_LOCK) + var lock: String? +) { + constructor(adModel: MkplAd) : this( + ownerId = adModel.ownerId.takeIf { it != MkplUserId.NONE }?.asString(), + id = adModel.id.takeIf { it != MkplAdId.NONE }?.asString(), + title = adModel.title.takeIf { it.isNotBlank() }, + description = adModel.description.takeIf { it.isNotBlank() }, + visibility = adModel.visibility.toTransport(), + productId = adModel.productId.takeIf { it != MkplProductId.NONE }?.asString(), + adType = adModel.adType.toTransport(), + lock = adModel.lock.takeIf { it != MkplAdLock.NONE }?.asString() + ) + + fun toAdModel(): MkplAd = + MkplAd( + ownerId = ownerId?.let { MkplUserId(it) } ?: MkplUserId.NONE, + id = id?.let { MkplAdId(it) } ?: MkplAdId.NONE, + title = title ?: "", + description = description ?: "", + visibility = visibility.fromTransport(), + productId = productId?.let { MkplProductId(it) } ?: MkplProductId.NONE, + adType = adType.fromTransport(), + lock = lock?.let { MkplAdLock(it) } ?: MkplAdLock.NONE + ) + + companion object { + const val TABLE_NAME = "ads" + + const val COLUMN_ID = "id" + const val COLUMN_TITLE = "title" + const val COLUMN_DESCRIPTION = "description" + const val COLUMN_OWNER_ID = "owner_id_my" + const val COLUMN_VISIBILITY = "visibility" + const val COLUMN_PRODUCT = "product" + const val COLUMN_AD_TYPE = "deal_side" + const val COLUMN_LOCK = "lock" + + fun table(keyspace: String, tableName: String) = + SchemaBuilder + .createTable(keyspace, tableName) + .ifNotExists() + .withPartitionKey(COLUMN_ID, DataTypes.TEXT) + .withColumn(COLUMN_TITLE, DataTypes.TEXT) + .withColumn(COLUMN_DESCRIPTION, DataTypes.TEXT) + .withColumn(COLUMN_OWNER_ID, DataTypes.TEXT) + .withColumn(COLUMN_VISIBILITY, DataTypes.TEXT) + .withColumn(COLUMN_PRODUCT, DataTypes.TEXT) + .withColumn(COLUMN_AD_TYPE, DataTypes.TEXT) + .withColumn(COLUMN_LOCK, DataTypes.TEXT) + .build() + + fun titleIndex(keyspace: String, tableName: String, locale: String = "en") = + SchemaBuilder + .createIndex() + .ifNotExists() + .usingSASI() + .onTable(keyspace, tableName) + .andColumn(COLUMN_TITLE) + .withSASIOptions(mapOf("mode" to "CONTAINS", "tokenization_locale" to locale)) + .build() + } +} diff --git a/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/model/AdDealSide.kt b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/model/AdDealSide.kt new file mode 100644 index 0000000..85d3061 --- /dev/null +++ b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/model/AdDealSide.kt @@ -0,0 +1,21 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.cassandra.model + +import ru.otus.otuskotlin.marketplace.common.models.MkplDealSide + +enum class AdDealSide { + DEMAND, + SUPPLY, +} + +fun AdDealSide?.fromTransport() = when(this) { + null -> MkplDealSide.NONE + AdDealSide.DEMAND -> MkplDealSide.DEMAND + AdDealSide.SUPPLY -> MkplDealSide.SUPPLY +} + +fun MkplDealSide.toTransport() = when(this) { + MkplDealSide.NONE -> null + MkplDealSide.DEMAND -> AdDealSide.DEMAND + MkplDealSide.SUPPLY -> AdDealSide.SUPPLY +} + diff --git a/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/model/AdVisibility.kt b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/model/AdVisibility.kt new file mode 100644 index 0000000..328e042 --- /dev/null +++ b/ok-marketplace-repo-cassandra/src/main/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/model/AdVisibility.kt @@ -0,0 +1,23 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.cassandra.model + +import ru.otus.otuskotlin.marketplace.common.models.MkplVisibility + +enum class AdVisibility { + VISIBLE_TO_OWNER, + VISIBLE_TO_GROUP, + VISIBLE_PUBLIC, +} + +fun AdVisibility?.fromTransport() = when(this) { + null -> MkplVisibility.NONE + AdVisibility.VISIBLE_TO_OWNER -> MkplVisibility.VISIBLE_TO_OWNER + AdVisibility.VISIBLE_TO_GROUP -> MkplVisibility.VISIBLE_TO_GROUP + AdVisibility.VISIBLE_PUBLIC -> MkplVisibility.VISIBLE_PUBLIC +} + +fun MkplVisibility.toTransport() = when(this) { + MkplVisibility.NONE -> null + MkplVisibility.VISIBLE_TO_OWNER -> AdVisibility.VISIBLE_TO_OWNER + MkplVisibility.VISIBLE_TO_GROUP -> AdVisibility.VISIBLE_TO_GROUP + MkplVisibility.VISIBLE_PUBLIC -> AdVisibility.VISIBLE_PUBLIC +} diff --git a/ok-marketplace-repo-cassandra/src/main/resources/META-INF/services/com.datastax.oss.driver.api.mapper.result.MapperResultProducerService b/ok-marketplace-repo-cassandra/src/main/resources/META-INF/services/com.datastax.oss.driver.api.mapper.result.MapperResultProducerService new file mode 100644 index 0000000..491ad3f --- /dev/null +++ b/ok-marketplace-repo-cassandra/src/main/resources/META-INF/services/com.datastax.oss.driver.api.mapper.result.MapperResultProducerService @@ -0,0 +1 @@ +ru.otus.otuskotlin.marketplace.backend.repo.cassandra.KotlinProducerService diff --git a/ok-marketplace-repo-cassandra/src/test/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/RepoAdCassandraTest.kt b/ok-marketplace-repo-cassandra/src/test/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/RepoAdCassandraTest.kt new file mode 100644 index 0000000..28c415d --- /dev/null +++ b/ok-marketplace-repo-cassandra/src/test/kotlin/ru/otus/otuskotlin/marketplace/backend/repo/cassandra/RepoAdCassandraTest.kt @@ -0,0 +1,46 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.cassandra + +import org.testcontainers.containers.CassandraContainer +import ru.otus.otuskotlin.marketplace.backend.repo.tests.* +import ru.otus.otuskotlin.marketplace.common.models.MkplAd +import ru.otus.otuskotlin.marketplace.common.models.MkplAdLock +import ru.otus.otuskotlin.marketplace.common.repo.IAdRepository +import java.time.Duration + +class RepoAdCassandraCreateTest : RepoAdCreateTest() { + override val repo: IAdRepository = TestCompanion.repository(initObjects, "ks_create", lockNew) +} + +class RepoAdCassandraDeleteTest : RepoAdDeleteTest() { + override val repo: IAdRepository = TestCompanion.repository(initObjects, "ks_delete", lockOld) +} + +class RepoAdCassandraReadTest : RepoAdReadTest() { + override val repo: IAdRepository = TestCompanion.repository(initObjects, "ks_read", MkplAdLock("")) +} + +class RepoAdCassandraSearchTest : RepoAdSearchTest() { + override val repo: IAdRepository = TestCompanion.repository(initObjects, "ks_search", MkplAdLock("")) +} + +class RepoAdCassandraUpdateTest : RepoAdUpdateTest() { + override val repo: IAdRepository = TestCompanion.repository(initObjects, "ks_update", lockNew) +} + +class TestCasandraContainer : CassandraContainer("cassandra:3.11.2") + +object TestCompanion { + private val container by lazy { + TestCasandraContainer().withStartupTimeout(Duration.ofSeconds(300L)) + .also { it.start() } + } + + fun repository(initObjects: List, keyspace: String, lock: MkplAdLock): RepoAdCassandra { + return RepoAdCassandra( + keyspaceName = keyspace, + host = container.host, + port = container.getMappedPort(CassandraContainer.CQL_PORT), + testing = true, randomUuid = { lock.asString() }, initObjects = initObjects + ) + } +} diff --git a/readme.md b/readme.md index eb7f55b..f692e3e 100644 --- a/readme.md +++ b/readme.md @@ -66,6 +66,7 @@ Marketplace -- это площадка, на которой пользовате 2. [ok-marketplace-repo-stubs](ok-marketplace-repo-stubs) Репозитарий-заглушка 3. [ok-marketplace-repo-tests](ok-marketplace-repo-tests) Проект с тестами для репозитариев 4. [ok-marketplace-repo-postgresql](ok-marketplace-repo-postgresql) Postgresql + 5. [ok-marketplace-repo-cassandra](ok-marketplace-repo-cassandra) Cassandra ## Подпроекты для занятий по языку Kotlin diff --git a/settings.gradle.kts b/settings.gradle.kts index 16af557..943501d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -65,7 +65,9 @@ include("ok-marketplace-app-kafka") include("ok-marketplace-lib-logging-common") include("ok-marketplace-lib-logging-kermit") include("ok-marketplace-lib-logging-logback") + include("ok-marketplace-repo-in-memory") include("ok-marketplace-repo-postgresql") include("ok-marketplace-repo-stubs") -include("ok-marketplace-repo-tests") \ No newline at end of file +include("ok-marketplace-repo-tests") +include("ok-marketplace-repo-cassandra") \ No newline at end of file