From df2850d6f81931b069b1956b0fe0ceb0716bbeb2 Mon Sep 17 00:00:00 2001 From: Sergey Okatov Date: Sun, 20 Aug 2023 21:37:39 +0500 Subject: [PATCH] =?UTF-8?q?m5l5=20-=20=D0=93=D1=80=D0=B0=D1=84=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=8F=20=D0=91=D0=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/docker-compose-ktor-gremlin.yml | 34 ++ gradle.properties | 6 +- ok-marketplace-repo-gremlin/README.MD | 7 + ok-marketplace-repo-gremlin/build.gradle.kts | 59 ++++ .../src/main/kotlin/AdGremlinConst.kt | 16 + .../src/main/kotlin/AdRepoGremlin.kt | 295 ++++++++++++++++++ .../DbDuplicatedElementsException.kt | 3 + .../kotlin/exceptions/WrongEnumException.kt | 3 + .../src/main/kotlin/mappers/AddMkplAd.kt | 91 ++++++ .../src/main/kotlin/mappers/Label.kt | 5 + .../test/kotlin/AdRepoGremlinCreateTest.kt | 20 ++ .../test/kotlin/AdRepoGremlinDeleteTest.kt | 22 ++ .../src/test/kotlin/AdRepoGremlinReadTest.kt | 19 ++ .../test/kotlin/AdRepoGremlinSearchTest.kt | 22 ++ .../test/kotlin/AdRepoGremlinUpdateTest.kt | 21 ++ .../src/test/kotlin/ArcadeDbContainer.kt | 33 ++ .../src/test/kotlin/TmpTest.kt | 145 +++++++++ .../src/commonMain/kotlin/runRepoTest.kt | 4 +- readme.md | 1 + settings.gradle.kts | 3 +- 20 files changed, 805 insertions(+), 4 deletions(-) create mode 100644 deploy/docker-compose-ktor-gremlin.yml create mode 100644 ok-marketplace-repo-gremlin/README.MD create mode 100644 ok-marketplace-repo-gremlin/build.gradle.kts create mode 100644 ok-marketplace-repo-gremlin/src/main/kotlin/AdGremlinConst.kt create mode 100644 ok-marketplace-repo-gremlin/src/main/kotlin/AdRepoGremlin.kt create mode 100644 ok-marketplace-repo-gremlin/src/main/kotlin/exceptions/DbDuplicatedElementsException.kt create mode 100644 ok-marketplace-repo-gremlin/src/main/kotlin/exceptions/WrongEnumException.kt create mode 100644 ok-marketplace-repo-gremlin/src/main/kotlin/mappers/AddMkplAd.kt create mode 100644 ok-marketplace-repo-gremlin/src/main/kotlin/mappers/Label.kt create mode 100644 ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinCreateTest.kt create mode 100644 ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinDeleteTest.kt create mode 100644 ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinReadTest.kt create mode 100644 ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinSearchTest.kt create mode 100644 ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinUpdateTest.kt create mode 100644 ok-marketplace-repo-gremlin/src/test/kotlin/ArcadeDbContainer.kt create mode 100644 ok-marketplace-repo-gremlin/src/test/kotlin/TmpTest.kt diff --git a/deploy/docker-compose-ktor-gremlin.yml b/deploy/docker-compose-ktor-gremlin.yml new file mode 100644 index 0000000..a9ae931 --- /dev/null +++ b/deploy/docker-compose-ktor-gremlin.yml @@ -0,0 +1,34 @@ +version: '3' +services: + gremlin: +# image: "arcadedata/arcadedb:23.4.1" + image: "arcadedata/arcadedb:23.7.1" + container_name: gremlin + ports: + - "2480:2480" + - "2424:2424" + - "8182:8182" + volumes: + - gremlin_data:/home/arcadedb/databases + environment: + JAVA_OPTS: "-Darcadedb.server.rootPassword=root_root -Darcadedb.server.plugins=GremlinServer:com.arcadedb.server.gremlin.GremlinServerPlugin -Darcadedb.server.defaultDatabases=mkpl[root1:playwithdata]" + +# app: +# image: "ok-marketplace-app-ktor:0.0.3" +# container_name: app-ktor +# ports: +# - "8080:8080" +## expose: +## - "8080" +# environment: +# DB_TYPE_PROD: gremlin +# DB_GREMLIN_HOST: gremlin +# DB_GREMLIN_PORT: 8182 +# DB_GREMLIN_USER: root +# DB_GREMLIN_PASS: root_root +# depends_on: +# - gremlin + +volumes: + gremlin_data: + diff --git a/gradle.properties b/gradle.properties index 8ed652a..fdf98df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -51,4 +51,8 @@ exposedVersion=0.41.1 kafkaVersion=3.4.0 # Cassandra -cassandraDriverVersion=4.13.0 \ No newline at end of file +cassandraDriverVersion=4.13.0 + +# Gremlin +tinkerpopVersion=3.7.0 +arcadeDbVersion=23.7.1 diff --git a/ok-marketplace-repo-gremlin/README.MD b/ok-marketplace-repo-gremlin/README.MD new file mode 100644 index 0000000..c0cc6bb --- /dev/null +++ b/ok-marketplace-repo-gremlin/README.MD @@ -0,0 +1,7 @@ +# Модуль `ok-marketplace-repo-gremlin` + + +Модуль реализует интерфейс репозитория ArcadeDb с вариантом Apache TinkerPop Gremlin. + +Поддерживается только JVM платформа. + diff --git a/ok-marketplace-repo-gremlin/build.gradle.kts b/ok-marketplace-repo-gremlin/build.gradle.kts new file mode 100644 index 0000000..737310d --- /dev/null +++ b/ok-marketplace-repo-gremlin/build.gradle.kts @@ -0,0 +1,59 @@ +import org.jetbrains.kotlin.gradle.internal.ensureParentDirsCreated + +plugins { + kotlin("jvm") +} + +val generatedPath = "$buildDir/generated/main/kotlin" +sourceSets { + main { + java.srcDir(generatedPath) + } +} + +dependencies { + val arcadeDbVersion: String by project + val tinkerpopVersion: String by project + val coroutinesVersion: String by project + val kmpUUIDVersion: String by project + val testContainersVersion: String by project + + implementation(project(":ok-marketplace-common")) + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") + implementation("com.benasher44:uuid:$kmpUUIDVersion") + + testImplementation(project(":ok-marketplace-repo-tests")) + + implementation(kotlin("stdlib-jdk8")) + implementation("org.apache.tinkerpop:gremlin-driver:$tinkerpopVersion") +// constraints { +// implementation("commons-collections:commons-collections:3.2.2") { +// because("Uncontrolled Recursion vulnerability pending CVSS allocation") +// } +// } + implementation("com.arcadedb:arcadedb-engine:$arcadeDbVersion") + implementation("com.arcadedb:arcadedb-network:$arcadeDbVersion") + implementation("com.arcadedb:arcadedb-gremlin:$arcadeDbVersion") + + testImplementation(kotlin("test-junit")) + testImplementation("org.testcontainers:testcontainers:$testContainersVersion") +} + +val arcadeDbVersion: String by project + +tasks { + val gradleConstants by creating { + file("$generatedPath/GradleConstants.kt").apply { + ensureParentDirsCreated() + writeText( + """ + package ru.otus.otuskotlin.marketplace.backend.repository.gremlin + + const val ARCADEDB_VERSION = "$arcadeDbVersion" + """.trimIndent() + ) + } + } + compileKotlin.get().dependsOn(gradleConstants) +} diff --git a/ok-marketplace-repo-gremlin/src/main/kotlin/AdGremlinConst.kt b/ok-marketplace-repo-gremlin/src/main/kotlin/AdGremlinConst.kt new file mode 100644 index 0000000..7cd976e --- /dev/null +++ b/ok-marketplace-repo-gremlin/src/main/kotlin/AdGremlinConst.kt @@ -0,0 +1,16 @@ +package ru.otus.otuskotlin.marketplace.backend.repository.gremlin + +object AdGremlinConst { + const val RESULT_SUCCESS = "success" + const val RESULT_LOCK_FAILURE = "lock-failure" + + const val FIELD_ID = "#id" + const val FIELD_TITLE = "title" + const val FIELD_DESCRIPTION = "description" + const val FIELD_AD_TYPE = "adType" + const val FIELD_OWNER_ID = "ownerId" + const val FIELD_VISIBILITY = "visibility" + const val FIELD_PRODUCT_ID = "productId" + const val FIELD_LOCK = "lock" + const val FIELD_TMP_RESULT = "_result" +} diff --git a/ok-marketplace-repo-gremlin/src/main/kotlin/AdRepoGremlin.kt b/ok-marketplace-repo-gremlin/src/main/kotlin/AdRepoGremlin.kt new file mode 100644 index 0000000..24bdb02 --- /dev/null +++ b/ok-marketplace-repo-gremlin/src/main/kotlin/AdRepoGremlin.kt @@ -0,0 +1,295 @@ +package ru.otus.otuskotlin.marketplace.backend.repository.gremlin + +import com.benasher44.uuid.uuid4 +import org.apache.tinkerpop.gremlin.driver.Cluster +import org.apache.tinkerpop.gremlin.driver.exception.ResponseException +import org.apache.tinkerpop.gremlin.driver.remote.DriverRemoteConnection +import org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource.traversal +import org.apache.tinkerpop.gremlin.process.traversal.TextP +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource +import org.apache.tinkerpop.gremlin.structure.Vertex +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.AdGremlinConst.FIELD_AD_TYPE +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.AdGremlinConst.FIELD_LOCK +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.AdGremlinConst.FIELD_OWNER_ID +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.AdGremlinConst.FIELD_TITLE +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.AdGremlinConst.FIELD_TMP_RESULT +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.AdGremlinConst.RESULT_LOCK_FAILURE +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.exceptions.DbDuplicatedElementsException +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.mappers.addMkplAd +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.mappers.label +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.mappers.listMkplAd +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.mappers.toMkplAd +import ru.otus.otuskotlin.marketplace.common.helpers.asMkplError +import ru.otus.otuskotlin.marketplace.common.helpers.errorAdministration +import ru.otus.otuskotlin.marketplace.common.helpers.errorRepoConcurrency +import ru.otus.otuskotlin.marketplace.common.models.* +import ru.otus.otuskotlin.marketplace.common.repo.* +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.`__` as gr + + +class AdRepoGremlin( + private val hosts: String, + private val port: Int = 8182, + private val enableSsl: Boolean = false, + private val user: String = "root", + private val pass: String = "", + initObjects: List = emptyList(), + initRepo: ((GraphTraversalSource) -> Unit)? = null, + val randomUuid: () -> String = { uuid4().toString() }, +) : IAdRepository { + + val initializedObjects: List + + private val cluster by lazy { + Cluster.build().apply { + addContactPoints(*hosts.split(Regex("\\s*,\\s*")).toTypedArray()) + port(port) + credentials(user, pass) + enableSsl(enableSsl) + }.create() + } + private val g by lazy { traversal().withRemote(DriverRemoteConnection.using(cluster)) } + + init { + if (initRepo != null) { + initRepo(g) + } + initializedObjects = initObjects.map { save(it) } + } + + private fun save(ad: MkplAd): MkplAd = g.addV(ad.label()) + .addMkplAd(ad) + .listMkplAd() + .next() + ?.toMkplAd() + ?: throw RuntimeException("Cannot initialize object $ad") + + override suspend fun createAd(rq: DbAdRequest): DbAdResponse { + val key = randomUuid() + val ad = rq.ad.copy(id = MkplAdId(key), lock = MkplAdLock(randomUuid())) + val dbRes = try { + g.addV(ad.label()) + .addMkplAd(ad) + .listMkplAd() + .toList() + } catch (e: Throwable) { + if (e is ResponseException || e.cause is ResponseException) { + return resultErrorNotFound(key) + } + return DbAdResponse( + data = null, + isSuccess = false, + errors = listOf(e.asMkplError()) + ) + } + return when (dbRes.size) { + 0 -> resultErrorNotFound(key) + 1 -> DbAdResponse( + data = dbRes.first().toMkplAd(), + isSuccess = true, + ) + + else -> errorDuplication(key) + } + } + + override suspend fun readAd(rq: DbAdIdRequest): DbAdResponse { + val key = rq.id.takeIf { it != MkplAdId.NONE }?.asString() ?: return resultErrorEmptyId + val dbRes = try { + g.V(key).listMkplAd().toList() + } catch (e: Throwable) { + if (e is ResponseException || e.cause is ResponseException) { + return resultErrorNotFound(key) + } + return DbAdResponse( + data = null, + isSuccess = false, + errors = listOf(e.asMkplError()) + ) + } + return when (dbRes.size) { + 0 -> resultErrorNotFound(key) + 1 -> DbAdResponse( + data = dbRes.first().toMkplAd(), + isSuccess = true, + ) + + else -> errorDuplication(key) + } + } + + override suspend fun updateAd(rq: DbAdRequest): DbAdResponse { + val key = rq.ad.id.takeIf { it != MkplAdId.NONE }?.asString() ?: return resultErrorEmptyId + val oldLock = rq.ad.lock.takeIf { it != MkplAdLock.NONE } ?: return resultErrorEmptyLock + val newLock = MkplAdLock(randomUuid()) + val newAd = rq.ad.copy(lock = newLock) + val dbRes = try { + g + .V(key) + .`as`("a") + .choose( + gr.select("a") + .values(FIELD_LOCK) + .`is`(oldLock.asString()), + gr.select("a").addMkplAd(newAd).listMkplAd(), + gr.select("a").listMkplAd(result = RESULT_LOCK_FAILURE) + ) + .toList() + } catch (e: Throwable) { + if (e is ResponseException || e.cause is ResponseException) { + return resultErrorNotFound(key) + } + return DbAdResponse( + data = null, + isSuccess = false, + errors = listOf(e.asMkplError()) + ) + } + val adResult = dbRes.firstOrNull()?.toMkplAd() + return when { + adResult == null -> resultErrorNotFound(key) + dbRes.size > 1 -> errorDuplication(key) + adResult.lock != newLock -> DbAdResponse( + data = adResult, + isSuccess = false, + errors = listOf( + errorRepoConcurrency( + expectedLock = oldLock, + actualLock = adResult.lock, + ), + ) + ) + + else -> DbAdResponse( + data = adResult, + isSuccess = true, + ) + } + } + + override suspend fun deleteAd(rq: DbAdIdRequest): DbAdResponse { + val key = rq.id.takeIf { it != MkplAdId.NONE }?.asString() ?: return resultErrorEmptyId + val oldLock = rq.lock.takeIf { it != MkplAdLock.NONE } ?: return resultErrorEmptyLock + val dbRes = try { + g + .V(key) + .`as`("a") + .choose( + gr.select("a") + .values(FIELD_LOCK) + .`is`(oldLock.asString()), + gr.select("a") + .sideEffect(gr.drop()) + .listMkplAd(), + gr.select("a") + .listMkplAd(result = RESULT_LOCK_FAILURE) + ) + .toList() + } catch (e: Throwable) { + if (e is ResponseException || e.cause is ResponseException) { + return resultErrorNotFound(key) + } + return DbAdResponse( + data = null, + isSuccess = false, + errors = listOf(e.asMkplError()) + ) + } + val dbFirst = dbRes.firstOrNull() + val adResult = dbFirst?.toMkplAd() + return when { + adResult == null -> resultErrorNotFound(key) + dbRes.size > 1 -> errorDuplication(key) + dbFirst[FIELD_TMP_RESULT] == RESULT_LOCK_FAILURE -> DbAdResponse( + data = adResult, + isSuccess = false, + errors = listOf( + errorRepoConcurrency( + expectedLock = oldLock, + actualLock = adResult.lock, + ), + ) + ) + + else -> DbAdResponse( + data = adResult, + isSuccess = true, + ) + } + } + + /** + * Поиск объявлений по фильтру + * Если в фильтре не установлен какой-либо из параметров - по нему фильтрация не идет + */ + override suspend fun searchAd(rq: DbAdFilterRequest): DbAdsResponse { + val result = try { + g.V() + .apply { rq.ownerId.takeIf { it != MkplUserId.NONE }?.also { has(FIELD_OWNER_ID, it.asString()) } } + .apply { rq.dealSide.takeIf { it != MkplDealSide.NONE }?.also { has(FIELD_AD_TYPE, it.name) } } + .apply { + rq.titleFilter.takeIf { it.isNotBlank() }?.also { has(FIELD_TITLE, TextP.containing(it)) } + } + .listMkplAd() + .toList() + } catch (e: Throwable) { + return DbAdsResponse( + isSuccess = false, + data = null, + errors = listOf(e.asMkplError()) + ) + } + return DbAdsResponse( + data = result.map { it.toMkplAd() }, + isSuccess = true + ) + } + + companion object { + val resultErrorEmptyId = DbAdResponse( + data = null, + isSuccess = false, + errors = listOf( + MkplError( + field = "id", + message = "Id must not be null or blank" + ) + ) + ) + val resultErrorEmptyLock = DbAdResponse( + data = null, + isSuccess = false, + errors = listOf( + MkplError( + field = "lock", + message = "Lock must be provided" + ) + ) + ) + + fun resultErrorNotFound(key: String, e: Throwable? = null) = DbAdResponse( + isSuccess = false, + data = null, + errors = listOf( + MkplError( + code = "not-found", + field = "id", + message = "Not Found object with key $key", + exception = e + ) + ) + ) + + fun errorDuplication(key: String) = DbAdResponse( + data = null, + isSuccess = false, + errors = listOf( + errorAdministration( + violationCode = "duplicateObjects", + description = "Database consistency failure", + exception = DbDuplicatedElementsException("Db contains multiple elements for id = '$key'") + ) + ) + ) + } +} diff --git a/ok-marketplace-repo-gremlin/src/main/kotlin/exceptions/DbDuplicatedElementsException.kt b/ok-marketplace-repo-gremlin/src/main/kotlin/exceptions/DbDuplicatedElementsException.kt new file mode 100644 index 0000000..145a4eb --- /dev/null +++ b/ok-marketplace-repo-gremlin/src/main/kotlin/exceptions/DbDuplicatedElementsException.kt @@ -0,0 +1,3 @@ +package ru.otus.otuskotlin.marketplace.backend.repository.gremlin.exceptions + +class DbDuplicatedElementsException(message: String) : Exception(message) diff --git a/ok-marketplace-repo-gremlin/src/main/kotlin/exceptions/WrongEnumException.kt b/ok-marketplace-repo-gremlin/src/main/kotlin/exceptions/WrongEnumException.kt new file mode 100644 index 0000000..0114d59 --- /dev/null +++ b/ok-marketplace-repo-gremlin/src/main/kotlin/exceptions/WrongEnumException.kt @@ -0,0 +1,3 @@ +package ru.otus.otuskotlin.marketplace.backend.repository.gremlin.exceptions + +class WrongEnumException(message: String) : Exception(message) diff --git a/ok-marketplace-repo-gremlin/src/main/kotlin/mappers/AddMkplAd.kt b/ok-marketplace-repo-gremlin/src/main/kotlin/mappers/AddMkplAd.kt new file mode 100644 index 0000000..52f0de8 --- /dev/null +++ b/ok-marketplace-repo-gremlin/src/main/kotlin/mappers/AddMkplAd.kt @@ -0,0 +1,91 @@ +package ru.otus.otuskotlin.marketplace.backend.repository.gremlin.mappers + +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.`__` as gr +import org.apache.tinkerpop.gremlin.structure.Vertex +import org.apache.tinkerpop.gremlin.structure.VertexProperty +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.AdGremlinConst.FIELD_AD_TYPE +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.AdGremlinConst.FIELD_DESCRIPTION +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.AdGremlinConst.FIELD_ID +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.AdGremlinConst.FIELD_LOCK +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.AdGremlinConst.FIELD_OWNER_ID +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.AdGremlinConst.FIELD_PRODUCT_ID +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.AdGremlinConst.FIELD_TITLE +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.AdGremlinConst.FIELD_TMP_RESULT +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.AdGremlinConst.FIELD_VISIBILITY +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.AdGremlinConst.RESULT_SUCCESS +import ru.otus.otuskotlin.marketplace.backend.repository.gremlin.exceptions.WrongEnumException +import ru.otus.otuskotlin.marketplace.common.models.* + +fun GraphTraversal.addMkplAd(ad: MkplAd): GraphTraversal = + this + .property(VertexProperty.Cardinality.single, FIELD_TITLE, ad.title.takeIf { it.isNotBlank() }) + .property(VertexProperty.Cardinality.single, FIELD_DESCRIPTION, ad.description.takeIf { it.isNotBlank() }) + .property(VertexProperty.Cardinality.single, FIELD_LOCK, ad.lock.takeIf { it != MkplAdLock.NONE }?.asString()) + .property( + VertexProperty.Cardinality.single, + FIELD_OWNER_ID, + ad.ownerId.asString().takeIf { it.isNotBlank() }) // здесь можно сделать ссылку на объект владельца + .property(VertexProperty.Cardinality.single, FIELD_AD_TYPE, ad.adType.takeIf { it != MkplDealSide.NONE }?.name) + .property( + VertexProperty.Cardinality.single, + FIELD_VISIBILITY, + ad.visibility.takeIf { it != MkplVisibility.NONE }?.name + ) + .property( + VertexProperty.Cardinality.single, + FIELD_PRODUCT_ID, + ad.productId.takeIf { it != MkplProductId.NONE }?.asString() + ) + +fun GraphTraversal.listMkplAd(result: String = RESULT_SUCCESS): GraphTraversal> = + project( + FIELD_ID, + FIELD_OWNER_ID, + FIELD_LOCK, + FIELD_TITLE, + FIELD_DESCRIPTION, + FIELD_AD_TYPE, + FIELD_VISIBILITY, + FIELD_PRODUCT_ID, + FIELD_TMP_RESULT, + ) + .by(gr.id()) + .by(FIELD_OWNER_ID) +// .by(gr.inE("Owns").outV().id()) + .by(FIELD_LOCK) + .by(FIELD_TITLE) + .by(FIELD_DESCRIPTION) + .by(FIELD_AD_TYPE) + .by(FIELD_VISIBILITY) + .by(FIELD_PRODUCT_ID) + .by(gr.constant(result)) +// .by(elementMap>()) + +fun Map.toMkplAd(): MkplAd = MkplAd( + id = (this[FIELD_ID] as? String)?.let { MkplAdId(it) } ?: MkplAdId.NONE, + ownerId = (this[FIELD_OWNER_ID] as? String)?.let { MkplUserId(it) } ?: MkplUserId.NONE, + lock = (this[FIELD_LOCK] as? String)?.let { MkplAdLock(it) } ?: MkplAdLock.NONE, + title = (this[FIELD_TITLE] as? String) ?: "", + description = (this[FIELD_DESCRIPTION] as? String) ?: "", + adType = when (val value = this[FIELD_AD_TYPE] as? String) { + MkplDealSide.SUPPLY.name -> MkplDealSide.SUPPLY + MkplDealSide.DEMAND.name -> MkplDealSide.DEMAND + null -> MkplDealSide.NONE + else -> throw WrongEnumException( + "Cannot convert object from DB. " + + "adType = '$value' cannot be converted to ${MkplDealSide::class}" + ) + }, + visibility = when (val value = this[FIELD_VISIBILITY]) { + MkplVisibility.VISIBLE_PUBLIC.name -> MkplVisibility.VISIBLE_PUBLIC + MkplVisibility.VISIBLE_TO_GROUP.name -> MkplVisibility.VISIBLE_TO_GROUP + MkplVisibility.VISIBLE_TO_OWNER.name -> MkplVisibility.VISIBLE_TO_OWNER + null -> MkplVisibility.NONE + else -> throw WrongEnumException( + "Cannot convert object from DB. " + + "visibility = '$value' cannot be converted to ${MkplVisibility::class}" + ) + }, + productId = (this[FIELD_PRODUCT_ID] as? String)?.let { MkplProductId(it) } ?: MkplProductId.NONE, +) diff --git a/ok-marketplace-repo-gremlin/src/main/kotlin/mappers/Label.kt b/ok-marketplace-repo-gremlin/src/main/kotlin/mappers/Label.kt new file mode 100644 index 0000000..757542a --- /dev/null +++ b/ok-marketplace-repo-gremlin/src/main/kotlin/mappers/Label.kt @@ -0,0 +1,5 @@ +package ru.otus.otuskotlin.marketplace.backend.repository.gremlin.mappers + +import ru.otus.otuskotlin.marketplace.common.models.MkplAd + +fun MkplAd.label(): String? = this::class.simpleName diff --git a/ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinCreateTest.kt b/ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinCreateTest.kt new file mode 100644 index 0000000..fdf685b --- /dev/null +++ b/ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinCreateTest.kt @@ -0,0 +1,20 @@ +package ru.otus.otuskotlin.marketplace.backend.repository.gremlin + +import ru.otus.otuskotlin.marketplace.backend.repo.tests.RepoAdCreateTest +import ru.otus.otuskotlin.marketplace.backend.repo.tests.RepoAdSearchTest +import ru.otus.otuskotlin.marketplace.common.repo.IAdRepository + +class AdRepoGremlinCreateTest : RepoAdCreateTest() { + override val repo: IAdRepository by lazy { + AdRepoGremlin( + hosts = ArcadeDbContainer.container.host, + port = ArcadeDbContainer.container.getMappedPort(8182), + enableSsl = false, + user = ArcadeDbContainer.username, + pass = ArcadeDbContainer.password, + initObjects = RepoAdSearchTest.initObjects, + initRepo = { g -> g.V().drop().iterate() }, + randomUuid = { lockNew.asString() } + ) + } +} diff --git a/ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinDeleteTest.kt b/ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinDeleteTest.kt new file mode 100644 index 0000000..166fd95 --- /dev/null +++ b/ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinDeleteTest.kt @@ -0,0 +1,22 @@ +package ru.otus.otuskotlin.marketplace.backend.repository.gremlin + +import ru.otus.otuskotlin.marketplace.backend.repo.tests.RepoAdDeleteTest +import ru.otus.otuskotlin.marketplace.common.models.MkplAd +import ru.otus.otuskotlin.marketplace.common.models.MkplAdId + +class AdRepoGremlinDeleteTest : RepoAdDeleteTest() { + override val repo: AdRepoGremlin by lazy { + AdRepoGremlin( + hosts = ArcadeDbContainer.container.host, + port = ArcadeDbContainer.container.getMappedPort(8182), + user = ArcadeDbContainer.username, + pass = ArcadeDbContainer.password, + enableSsl = false, + initObjects = initObjects, + initRepo = { g -> g.V().drop().iterate() }, + ) + } + override val deleteSucc: MkplAd by lazy { repo.initializedObjects[0] } + override val deleteConc: MkplAd by lazy { repo.initializedObjects[1] } + override val notFoundId: MkplAdId = MkplAdId("#3100:0") +} diff --git a/ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinReadTest.kt b/ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinReadTest.kt new file mode 100644 index 0000000..02a2693 --- /dev/null +++ b/ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinReadTest.kt @@ -0,0 +1,19 @@ +package ru.otus.otuskotlin.marketplace.backend.repository.gremlin + +import ru.otus.otuskotlin.marketplace.backend.repo.tests.RepoAdReadTest +import ru.otus.otuskotlin.marketplace.common.models.MkplAd + +class AdRepoGremlinReadTest : RepoAdReadTest() { + override val repo: AdRepoGremlin by lazy { + AdRepoGremlin( + hosts = ArcadeDbContainer.container.host, + port = ArcadeDbContainer.container.getMappedPort(8182), + user = ArcadeDbContainer.username, + pass = ArcadeDbContainer.password, + enableSsl = false, + initObjects = initObjects, + initRepo = { g -> g.V().drop().iterate() }, + ) + } + override val readSucc: MkplAd by lazy { repo.initializedObjects[0] } +} diff --git a/ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinSearchTest.kt b/ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinSearchTest.kt new file mode 100644 index 0000000..6f53cc3 --- /dev/null +++ b/ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinSearchTest.kt @@ -0,0 +1,22 @@ +package ru.otus.otuskotlin.marketplace.backend.repository.gremlin + +import ru.otus.otuskotlin.marketplace.backend.repo.tests.RepoAdSearchTest +import ru.otus.otuskotlin.marketplace.common.models.MkplAd + +class AdRepoGremlinSearchTest: RepoAdSearchTest() { + override val repo: AdRepoGremlin by lazy { + AdRepoGremlin( + hosts = ArcadeDbContainer.container.host, + port = ArcadeDbContainer.container.getMappedPort(8182), + enableSsl = false, + user = ArcadeDbContainer.username, + pass = ArcadeDbContainer.password, + initObjects = initObjects, + initRepo = { g -> g.V().drop().iterate() }, + ) + } + + override val initializedObjects: List by lazy { + repo.initializedObjects + } +} diff --git a/ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinUpdateTest.kt b/ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinUpdateTest.kt new file mode 100644 index 0000000..1068d08 --- /dev/null +++ b/ok-marketplace-repo-gremlin/src/test/kotlin/AdRepoGremlinUpdateTest.kt @@ -0,0 +1,21 @@ +package ru.otus.otuskotlin.marketplace.backend.repository.gremlin + +import ru.otus.otuskotlin.marketplace.backend.repo.tests.RepoAdUpdateTest +import ru.otus.otuskotlin.marketplace.common.models.MkplAd + +class AdRepoGremlinUpdateTest: RepoAdUpdateTest() { + override val repo: AdRepoGremlin by lazy { + AdRepoGremlin( + hosts = ArcadeDbContainer.container.host, + port = ArcadeDbContainer.container.getMappedPort(8182), + enableSsl = false, + user = ArcadeDbContainer.username, + pass = ArcadeDbContainer.password, + initObjects = initObjects, + initRepo = { g -> g.V().drop().iterate() }, + randomUuid = { lockNew.asString() }, + ) + } + override val updateSucc: MkplAd by lazy { repo.initializedObjects[0] } + override val updateConc: MkplAd by lazy { repo.initializedObjects[1] } +} diff --git a/ok-marketplace-repo-gremlin/src/test/kotlin/ArcadeDbContainer.kt b/ok-marketplace-repo-gremlin/src/test/kotlin/ArcadeDbContainer.kt new file mode 100644 index 0000000..fc5ff2c --- /dev/null +++ b/ok-marketplace-repo-gremlin/src/test/kotlin/ArcadeDbContainer.kt @@ -0,0 +1,33 @@ +package ru.otus.otuskotlin.marketplace.backend.repository.gremlin + +import org.testcontainers.containers.GenericContainer +import org.testcontainers.containers.wait.strategy.Wait +import org.testcontainers.utility.DockerImageName +import java.time.Duration + +object ArcadeDbContainer { + val username: String = "root" + val password: String = "root_root" + val container by lazy { + GenericContainer(DockerImageName.parse("arcadedata/arcadedb:${ARCADEDB_VERSION}")).apply { + withExposedPorts(2480, 2424, 8182) + withEnv( + "JAVA_OPTS", "-Darcadedb.server.rootPassword=$password " + + "-Darcadedb.server.plugins=GremlinServer:com.arcadedb.server.gremlin.GremlinServerPlugin" + ) +// // Строчки ниже почему-то не работают больше +// withEnv("arcadedb.server.rootPassword", "1r2d3g4h@5j6k7l8p") +// withEnv("arcadedb.server.plugins", "GremlinServer:com.arcadedb.server.gremlin.GremlinServerPlugin") +// withEnv("arcadedb.server.defaultDatabases", "OpenBeer[root]{import:https://github.com/ArcadeData/arcadedb-datasets/raw/main/orientdb/OpenBeer.gz}") + waitingFor(Wait.forLogMessage(".*ArcadeDB Server started.*\\n", 1)) + withStartupTimeout(Duration.ofMinutes(5)) + start() + println("ARCADE: http://${host}:${getMappedPort(2480)}") + println("ARCADE: http://${host}:${getMappedPort(2424)}") + println("ARCADE: http://${host}:${getMappedPort(8182)}") + Thread.sleep(1000) + println(this.logs) + println("RUNNING?: ${this.isRunning}") + } + } +} diff --git a/ok-marketplace-repo-gremlin/src/test/kotlin/TmpTest.kt b/ok-marketplace-repo-gremlin/src/test/kotlin/TmpTest.kt new file mode 100644 index 0000000..f533814 --- /dev/null +++ b/ok-marketplace-repo-gremlin/src/test/kotlin/TmpTest.kt @@ -0,0 +1,145 @@ +package ru.otus.otuskotlin.marketplace.backend.repository.gremlin + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.apache.tinkerpop.gremlin.driver.AuthProperties +import org.apache.tinkerpop.gremlin.driver.Cluster +import org.apache.tinkerpop.gremlin.driver.remote.DriverRemoteConnection +import org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource.traversal +import org.apache.tinkerpop.gremlin.structure.Vertex +import org.junit.Ignore +import org.junit.Test +import ru.otus.otuskotlin.marketplace.common.models.MkplAd +import ru.otus.otuskotlin.marketplace.common.models.MkplUserId +import ru.otus.otuskotlin.marketplace.common.repo.DbAdIdRequest +import ru.otus.otuskotlin.marketplace.common.repo.DbAdRequest +import kotlin.test.fail +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.`__` as bs + + +@Ignore("Тест для экспериментов") +class TmpTest { + + @Test + fun z() { +// val db = RemoteDatabase("localhost", 2480, "mkpl", "root", "root_root").use { +// val z = db.command("gremlin", "g.V()") +// println("ZZL: ${z.stream().map { it.toJSON() }.asSequence().joinToString("\n")}") +// } + + val authProp = AuthProperties().apply { + with(AuthProperties.Property.USERNAME, "root") + with(AuthProperties.Property.PASSWORD, "root_root") + } + val cluster = Cluster.build() + .addContactPoints("localhost") + .port(8182) + .credentials("root1", "playwithdata") +// .authProperties(authProp) + .create() + // Должно работать таким образом, но на текущий момент не работает из-за бага в ArcadeDb + // Тест в ArcadeDb задизейблен: + // https://github.com/ArcadeData/arcadedb/blob/main/gremlin/src/test/java/com/arcadedb/server/gremlin/ConnectRemoteGremlinServer.java + traversal() + .withRemote(DriverRemoteConnection.using(cluster, "g")) + .use { g -> + val userId = g + .addV("User") + .property("name", "Evan") + .next() + .id() + println("UserID: $userId") + } + } + + @Test + fun y() { + val host = "localhost" + val port = 8182 + val cluster = Cluster.build().apply { + addContactPoints(host) + port(port) +// credentials("root", "root_root") + credentials("root1", "playwithdata") + path("/gremlin") +// path("/mkpl") +// enableSsl(enableSsl) + }.create() + val g = traversal() + .withRemote(DriverRemoteConnection.using(cluster)) + val x = g.V().hasLabel("Test").`as`("a") + .project("lock", "ownerId", "z") + .by("lock") + .by(bs.inE("Owns").outV().id()) + .by(bs.elementMap>()) + .toList() + + println("CONTENT: ${x}") + g.close() + } + + @Test + fun x() { +// val host = ArcadeDbContainer.container.host +// val port = ArcadeDbContainer.container.getMappedPort(8182) + val host = "localhost" + val port = 8182 + val cluster = Cluster.build().apply { + addContactPoints(host) + port(port) + credentials("root", "root_root") +// path("/mkpl") +// enableSsl(enableSsl) + }.create() + traversal().withRemote(DriverRemoteConnection.using(cluster)).use { g -> + val userId = g + .addV("User") + .property("name", "Ivan") + .next() + .id() + println("UserID: $userId") + + val id = g + .addV("Test") + .`as`("a") + .property("lock", "111") + .addE("Owns") + .from(bs.V(userId)) + .select("a") + .next() + .id() + println("ID: $id") + + val owner = g + .V(userId) + .outE("Owns") + .where(bs.inV().id().`is`(id)) + .toList() + println("OWNER: $owner") + +// val n = g +// .V(id) +// .`as`("a") +// .choose( +// bs.select("a") +// .values("lock") +// .`is`("1112"), +// bs.select("a").drop().inject("success"), +// bs.constant("lock-failure") +// ).toList() +// println("YYY: $n") + +// val x = g.V(id).`as`("a") +// .union( +// bs.select("a") +// .inE("User") +// .outV() +// .V(), +// bs.select("a") +// ) +// .elementMap() +// .toList() +// println("CONTENT: ${x}") + } + } +} diff --git a/ok-marketplace-repo-tests/src/commonMain/kotlin/runRepoTest.kt b/ok-marketplace-repo-tests/src/commonMain/kotlin/runRepoTest.kt index fa0a2d6..36c4611 100644 --- a/ok-marketplace-repo-tests/src/commonMain/kotlin/runRepoTest.kt +++ b/ok-marketplace-repo-tests/src/commonMain/kotlin/runRepoTest.kt @@ -7,8 +7,8 @@ import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext @OptIn(ExperimentalCoroutinesApi::class) -fun runRepoTest(testBody: suspend TestScope.() -> Unit) = runTest { +fun runRepoTest(testBody: suspend TestScope.() -> Unit) = runTest(dispatchTimeoutMs = 5*60*1000) { withContext(Dispatchers.Default) { testBody() } -} \ No newline at end of file +} diff --git a/readme.md b/readme.md index f692e3e..51a3054 100644 --- a/readme.md +++ b/readme.md @@ -67,6 +67,7 @@ Marketplace -- это площадка, на которой пользовате 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 + 6. [ok-marketplace-repo-gremlin](ok-marketplace-repo-gremlin) ArcadeDb ## Подпроекты для занятий по языку Kotlin diff --git a/settings.gradle.kts b/settings.gradle.kts index 943501d..ded96d1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -70,4 +70,5 @@ include("ok-marketplace-repo-in-memory") include("ok-marketplace-repo-postgresql") include("ok-marketplace-repo-stubs") include("ok-marketplace-repo-tests") -include("ok-marketplace-repo-cassandra") \ No newline at end of file +include("ok-marketplace-repo-cassandra") +include("ok-marketplace-repo-gremlin")