diff --git a/gradle/diktat.yml b/gradle/diktat.yml index 3fdb54cb..625c70e9 100644 --- a/gradle/diktat.yml +++ b/gradle/diktat.yml @@ -17,4 +17,8 @@ - name: TOO_MANY_PARAMETERS enabled: true configuration: - maxParameterListSize: '10' \ No newline at end of file + maxParameterListSize: '10' +- name: FILE_NAME_INCORRECT + enabled: false +- name: LOCAL_VARIABLE_EARLY_DECLARATION + enabled: false \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e2f41574..b05d973f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,8 +13,8 @@ jmh-plugin="0.7.2" diktat-plugin="1.0.1" jmhreport-plugin="0.9.0" # Library dependencies -kotlin-stdlib="1.9.22" -kotlinx-coroutines="1.7.3" +kotlin-stdlib="1.9.24" +kotlinx-coroutines="1.8.0" jupiter="5.10.2" mockito="5.11.0" mockito-kotlin="5.3.1" @@ -27,6 +27,8 @@ video-recorder="2.0" gson="2.10.1" mongodb-driver="5.1.0" owasp-encoder="1.2.3" +testContainers="1.19.8" +intellij-database="241.15989.150" [plugins] intellij={ id="org.jetbrains.intellij", version.ref="intellij-plugin" } @@ -45,6 +47,14 @@ kotlin-coroutines-core={ group="org.jetbrains.kotlinx", name="kotlinx-coroutines kotlin-coroutines-swing={ group="org.jetbrains.kotlinx", name="kotlinx-coroutines-swing", version.ref="kotlinx-coroutines" } kotlin-coroutines-test={ group="org.jetbrains.kotlinx", name="kotlinx-coroutines-test", version.ref="kotlinx-coroutines" } ###################################################### +## IntelliJ compileOnly libraries. They must not be bundled because they are already part of the +## JetBrains ecosystem. +intellij-database-sql={ group="com.jetbrains.intellij.database", name="database-sql", version.ref="intellij-database" } +intellij-database-connectivity={ group="com.jetbrains.intellij.database", name="database-connectivity", version.ref="intellij-database" } +intellij-database-jdbc-console={ group="com.jetbrains.intellij.database", name="database-jdbc-console", version.ref="intellij-database" } +intellij-database-core-base={ group="com.jetbrains.intellij.database", name="database", version.ref="intellij-database" } +intellij-database-core-impl={ group="com.jetbrains.intellij.database", name="database-core-impl", version.ref="intellij-database" } +###################################################### ## Production Libraries. segment={ group="com.segment.analytics.java", name="analytics", version.ref="segment" } gson={ group="com.google.code.gson", name="gson", version.ref="gson" } @@ -62,13 +72,16 @@ testing-remoteRobot={ group="com.intellij.remoterobot", name="remote-robot", ver testing-remoteRobotDeps-remoteFixtures={ group="com.intellij.remoterobot", name="remote-fixtures", version.ref="intellij-remoteRobot"} testing-remoteRobotDeps-ideLauncher={ group="com.intellij.remoterobot", name="ide-launcher", version.ref="intellij-remoteRobot"} testing-remoteRobotDeps-okHttp={ group="com.squareup.okhttp3", name="okhttp", version.ref="okHttp" } -testing-intellij-ideImpl={ group="com.jetbrains.intellij.platform", name="ide-impl", version.ref="intellij-testBuild" } +testing-intellij-ideImpl={ group="com.jetbrains.intellij.platform", name="ide", version.ref="intellij-testBuild" } testing-intellij-coreUi={ group="com.jetbrains.intellij.platform", name="core-ui", version.ref="intellij-testBuild" } testing-remoteRobotDeps-retrofit={ group="com.squareup.retrofit2", name="retrofit", version.ref="retrofit" } testing-remoteRobotDeps-retrofitGson={ group="com.squareup.retrofit2", name="converter-gson", version.ref="retrofit" } testing-jmh-core={ group="org.openjdk.jmh", name="jmh-core", version.ref="jmh" } testing-jmh-annotationProcessor={ group="org.openjdk.jmh", name="jmh-generator-annprocess", version.ref="jmh" } testing-jmh-generatorByteCode={ group="org.openjdk.jmh", name="jmh-generator-bytecode", version.ref="jmh" } +testing-testContainers-core= { group="org.testcontainers", name="testcontainers", version.ref="testContainers" } +testing-testContainers-mongodb= { group="org.testcontainers", name="mongodb", version.ref="testContainers" } +testing-testContainers-jupiter= { group="org.testcontainers", name="junit-jupiter", version.ref="testContainers"} ###################################################### ## Libraries and plugins only used for the buildScript. buildScript-plugin-kotlin={ group="org.jetbrains.kotlin", name="kotlin-gradle-plugin", version="1.9.23" } diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/actions/GetMongoDBVersionAction.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/actions/GetMongoDBVersionAction.kt similarity index 68% rename from packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/actions/GetMongoDBVersionAction.kt rename to packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/actions/GetMongoDBVersionAction.kt index 25d5eb4b..c08b4f91 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/actions/GetMongoDBVersionAction.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/actions/GetMongoDBVersionAction.kt @@ -1,4 +1,9 @@ -package com.mongodb.jbplugin.observability.actions +/** + * A Simple, example action, that prints out in a modal popup the version of the + * connected MongoDB Cluster. + */ + +package com.mongodb.jbplugin.actions import com.intellij.database.dataSource.localDataSource import com.intellij.database.psi.DbDataSource @@ -9,19 +14,24 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.rd.util.launchChildOnUi import com.intellij.openapi.ui.Messages import com.mongodb.jbplugin.accessadapter.datagrip.DataGripBasedReadModelProvider -import com.mongodb.jbplugin.accessadapter.slice.BuildInfoSlice +import com.mongodb.jbplugin.accessadapter.slice.BuildInfo import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +/** + * Service that implements the action. + * + * @param coroutineScope + */ @Service(Service.Level.PROJECT) -class GetMongoDBVersionActionService( +class GetMongoDbVersionActionService( private val coroutineScope: CoroutineScope ) { fun actionPerformed(event: AnActionEvent) { coroutineScope.launch { val readModelProvider = event.project!!.getService(DataGripBasedReadModelProvider::class.java) val dataSource = event.dataContext.getData(PlatformDataKeys.PSI_ELEMENT) as DbDataSource - val buildInfo = readModelProvider.slice(dataSource.localDataSource!!, BuildInfoSlice) + val buildInfo = readModelProvider.slice(dataSource.localDataSource!!, BuildInfo.Slice) coroutineScope.launchChildOnUi { Messages.showMessageDialog(buildInfo.version, "Show DB Version", null) @@ -30,8 +40,11 @@ class GetMongoDBVersionActionService( } } -class GetMongoDBVersionAction: AnAction() { +/** + * Action that can be run within the contextual menu of a connection in the data explorer. + */ +class GetMongoDbVersionAction : AnAction() { override fun actionPerformed(event: AnActionEvent) { - event.project!!.getService(GetMongoDBVersionActionService::class.java).actionPerformed(event) + event.project!!.getService(GetMongoDbVersionActionService::class.java).actionPerformed(event) } } \ No newline at end of file diff --git a/packages/jetbrains-plugin/src/main/resources/META-INF/plugin.xml b/packages/jetbrains-plugin/src/main/resources/META-INF/plugin.xml index 07f46cd7..8be472b9 100644 --- a/packages/jetbrains-plugin/src/main/resources/META-INF/plugin.xml +++ b/packages/jetbrains-plugin/src/main/resources/META-INF/plugin.xml @@ -16,7 +16,7 @@ + class="com.mongodb.jbplugin.actions.GetMongoDbVersionAction" text="Show MongoDB Version"> diff --git a/packages/mongodb-access-adapter/datagrip-access-adapter/build.gradle.kts b/packages/mongodb-access-adapter/datagrip-access-adapter/build.gradle.kts index 444e9691..5d8bf996 100644 --- a/packages/mongodb-access-adapter/datagrip-access-adapter/build.gradle.kts +++ b/packages/mongodb-access-adapter/datagrip-access-adapter/build.gradle.kts @@ -1,22 +1,53 @@ + + repositories { maven("https://www.jetbrains.com/intellij-repository/releases/") + maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") +} + +plugins { + alias(libs.plugins.intellij) +} + + +tasks { + named("test", Test::class) { + environment("TESTCONTAINERS_RYUK_DISABLED", "true") + val homePath = project.layout.buildDirectory.dir("idea-sandbox/config-test").get().asFile.absolutePath + + jvmArgs(listOf( + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.desktop/java.awt=ALL-UNNAMED", + "--add-opens=java.desktop/javax.swing=ALL-UNNAMED", + "--add-opens=java.desktop/sun.awt=ALL-UNNAMED", + "-Dpolyglot.engine.WarnInterpreterOnly=false", + "-Dpolyglot.log.level=OFF", + "-Didea.home.path=${homePath}" + )) + } } + +intellij { + version.set(libs.versions.intellij.min) // Target IDE Version + type.set(libs.versions.intellij.type) // Target IDE Platform + + plugins.set(listOf("com.intellij.java", "com.intellij.database")) +} + dependencies { implementation(libs.gson) implementation(libs.mongodb.driver) implementation(project(":packages:mongodb-access-adapter")) - compileOnly(libs.testing.intellij.ideImpl) - compileOnly(libs.testing.intellij.coreUi) - compileOnly("com.jetbrains.intellij.database:database-sql:241.15989.150") - compileOnly("com.jetbrains.intellij.database:database-connectivity:241.15989.150") - compileOnly("com.jetbrains.intellij.database:database-core-impl:241.15989.150") { - exclude("com.jetbrains.fus.reporting", "ap-validation") + testImplementation("com.jetbrains.intellij.platform:test-framework-junit5:241.15989.155") { + exclude("ai.grazie.spell") + exclude("ai.grazie.utils") + exclude("ai.grazie.nlp") + exclude("ai.grazie.model") + exclude("org.jetbrains.teamcity") } - compileOnly("com.jetbrains.intellij.database:database-jdbc-console:241.15989.150") - compileOnly("com.jetbrains.intellij.database:database:241.15989.150") - compileOnly("com.jetbrains.intellij.platform:images:241.15989.157") - compileOnly("com.jetbrains.intellij.grid:grid-impl:241.15989.150") - compileOnly("com.jetbrains.intellij.grid:grid:241.15989.150") - compileOnly("com.jetbrains.intellij.grid:grid-core-impl:241.15989.150") + + testImplementation(libs.testing.testContainers.core) + testImplementation(libs.testing.testContainers.jupiter) + testImplementation(libs.testing.testContainers.mongodb) } \ No newline at end of file diff --git a/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/DataGripBasedReadModelProvider.kt b/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/DataGripBasedReadModelProvider.kt index 2eefb4e0..122419f5 100644 --- a/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/DataGripBasedReadModelProvider.kt +++ b/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/DataGripBasedReadModelProvider.kt @@ -1,3 +1,8 @@ +/** + * Represents a service that allows access to a MongoDB cluster + * configured through a DataGrip DataSource. + */ + package com.mongodb.jbplugin.accessadapter.datagrip import com.intellij.database.dataSource.LocalDataSource @@ -6,30 +11,47 @@ import com.intellij.openapi.project.Project import com.intellij.psi.util.CachedValue import com.intellij.psi.util.CachedValueProvider import com.intellij.psi.util.CachedValuesManager -import com.mongodb.jbplugin.accessadapter.MongoDBReadModelProvider +import com.mongodb.jbplugin.accessadapter.MongoDbReadModelProvider import com.mongodb.jbplugin.accessadapter.Slice -import com.mongodb.jbplugin.accessadapter.datagrip.adapter.DataGripMongoDBDriver -import com.mongodb.jbplugin.accessadapter.slice.BuildInfoSlice +import com.mongodb.jbplugin.accessadapter.datagrip.adapter.DataGripMongoDbDriver import kotlinx.coroutines.runBlocking +private typealias MapOfCachedValues = MutableMap> + +/** + * The service to be injected to access MongoDB. Usually you will use + * it like this: + * + * ```kt + * val readModelProvider = event.project!!.getService(DataGripBasedReadModelProvider::class.java) + * val dataSource = event.dataContext.getData(PlatformDataKeys.PSI_ELEMENT) as DbDataSource + * val buildInfo = readModelProvider.slice(dataSource.localDataSource!!, BuildInfoSlice) + * ``` + * + * It will aggressively cache data at the project level, to avoid hitting MongoDB. Also, the provided + * driver is very slow, so it's better to avoid querying on performance sensitive contexts. + * + * @param project + */ @Service(Service.Level.PROJECT) class DataGripBasedReadModelProvider( private val project: Project, -) : MongoDBReadModelProvider { - private val cachedValues: MutableMap> = mutableMapOf() +) : MongoDbReadModelProvider { + private val cachedValues: MapOfCachedValues = mutableMapOf() - override fun slice(dataSource: LocalDataSource, slice: Slice): T { - return cachedValues - .computeIfAbsent(slice.javaClass.canonicalName, fromSlice(dataSource, BuildInfoSlice)) + override fun slice(dataSource: LocalDataSource, slice: Slice): T = cachedValues + .computeIfAbsent(slice.javaClass.canonicalName, fromSlice(dataSource, slice)) .value as T - } - private inline fun fromSlice(dataSource: LocalDataSource, slice: Slice): (String) -> CachedValue { + private fun fromSlice( + dataSource: LocalDataSource, + slice: Slice + ): (String) -> CachedValue { val cacheManager = CachedValuesManager.getManager(project) return { cacheManager.createCachedValue { runBlocking { - val driver = DataGripMongoDBDriver(project, dataSource) + val driver = DataGripMongoDbDriver(project, dataSource) val sliceData = slice.queryUsingDriver(driver) CachedValueProvider.Result.create(sliceData, dataSource) diff --git a/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDBDriver.kt b/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDBDriver.kt deleted file mode 100644 index 3ad4ef6e..00000000 --- a/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDBDriver.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.mongodb.jbplugin.accessadapter.datagrip.adapter - -import com.intellij.database.dataSource.LocalDataSource -import com.intellij.openapi.project.Project -import com.mongodb.jbplugin.accessadapter.MongoDBDriver -import com.mongodb.jbplugin.accessadapter.Namespace -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.bson.Document -import kotlin.reflect.KClass -import kotlin.time.Duration - -class DataGripMongoDBDriver( - val project: Project, - val dataSource: LocalDataSource -) : MongoDBDriver { - override suspend fun runCommand(command: Document, result: KClass, timeout: Duration): T = withContext(Dispatchers.IO) { - DataSourceQuery(project, dataSource, result).runQuery( - """db.runCommand(${command.toJson()})""", - timeout - )[0] - } - - override suspend fun findOne( - namespace: Namespace, - query: Document, - options: Document, - result: KClass, - timeout: Duration - ): T? = withContext(Dispatchers.IO) { - DataSourceQuery(project, dataSource, result).runQuery( - """db.getSiblingDB("${namespace.database}") - .getCollection("${namespace.collection}") - .findOne(${query.toJson()}, ${options.toJson()}) """.trimMargin(), - timeout - ).getOrNull(0) - } - - override suspend fun findAll( - namespace: Namespace, - query: Document, - result: KClass, - limit: Int, - timeout: Duration - ) = withContext(Dispatchers.IO) { - DataSourceQuery(project, dataSource, result).runQuery( - """db.getSiblingDB("${namespace.database}") - .getCollection("${namespace.collection}") - .find(${query.toJson()}).limit(${limit}) """.trimMargin(), - timeout - ) - } -} \ No newline at end of file diff --git a/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriver.kt b/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriver.kt new file mode 100644 index 00000000..84bcd338 --- /dev/null +++ b/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriver.kt @@ -0,0 +1,129 @@ +/** + * Represents a MongoDB driver interface that uses a DataGrip + * connection to query MongoDB. + */ + +package com.mongodb.jbplugin.accessadapter.datagrip.adapter + +import com.google.gson.Gson +import com.intellij.database.dataSource.DatabaseConnection +import com.intellij.database.dataSource.DatabaseConnectionManager +import com.intellij.database.dataSource.LocalDataSource +import com.intellij.database.dataSource.connection.ConnectionRequestor +import com.intellij.database.run.ConsoleRunConfiguration +import com.intellij.openapi.project.Project +import com.mongodb.jbplugin.accessadapter.MongoDbDriver +import com.mongodb.jbplugin.accessadapter.Namespace +import org.bson.Document + +import kotlin.reflect.KClass +import kotlin.time.Duration +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout + +/** + * The driver itself. Shouldn't be used directly, but through the + * DataGripBasedReadModelProvider. + * + * @see com.mongodb.jbplugin.accessadapter.datagrip.DataGripBasedReadModelProvider + * + * @param project + * @param dataSource + */ +internal class DataGripMongoDbDriver( + private val project: Project, + private val dataSource: LocalDataSource +) : MongoDbDriver { + private val gson = Gson() + + override suspend fun runCommand( +command: Document, + result: KClass, + timeout: Duration +): T = withContext( +Dispatchers.IO +) { + runQuery( + """db.runCommand(${command.toJson()})""", + result, + timeout + )[0] + } + + override suspend fun findOne( + namespace: Namespace, + query: Document, + options: Document, + result: KClass, + timeout: Duration + ): T? = withContext(Dispatchers.IO) { + runQuery( + """db.getSiblingDB("${namespace.database}") + .getCollection("${namespace.collection}") + .findOne(${query.toJson()}, ${options.toJson()}) """.trimMargin(), + result, + timeout + ).getOrNull(0) + } + + override suspend fun findAll( + namespace: Namespace, + query: Document, + result: KClass, + limit: Int, + timeout: Duration + ) = withContext(Dispatchers.IO) { + runQuery( + """db.getSiblingDB("${namespace.database}") + .getCollection("${namespace.collection}") + .find(${query.toJson()}).limit($limit) """.trimMargin(), + result, + timeout + ) + } + + suspend fun runQuery( +queryString: String, + resultClass: KClass, + timeout: Duration +): List = + withContext(Dispatchers.IO) { + val connection = getConnection() + val remoteConnection = connection.remoteConnection + val statement = remoteConnection.prepareStatement(queryString.trimIndent()) + + withTimeout(timeout) { + val listOfResults = mutableListOf() + val resultSet = statement.executeQuery() + + if (resultClass.java.isPrimitive || resultClass == String::class.java) { + while (resultSet.next()) { + listOfResults.add(resultSet.getObject(1) as T) + } + } else { + while (resultSet.next()) { + val hashMap = resultSet.getObject(1) as Map + val result = gson.fromJson(gson.toJson(hashMap), resultClass.java) + listOfResults.add(result) + } + } + + listOfResults + } + } + + private suspend fun getConnection(): DatabaseConnection { + val connections = DatabaseConnectionManager.getInstance().activeConnections + return connections.firstOrNull { it.connectionPoint.dataSource == dataSource } + ?: DatabaseConnectionManager.establishConnection( + dataSource, + ConsoleRunConfiguration.newConfiguration(project).apply { + setOptionsFromDataSource(dataSource) + }, + ConnectionRequestor.Anonymous(), + project, + true // if password is not available + )!! + } +} \ No newline at end of file diff --git a/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripQueryAdapter.kt b/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripQueryAdapter.kt deleted file mode 100644 index 0ff5e3a0..00000000 --- a/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripQueryAdapter.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.mongodb.jbplugin.accessadapter.datagrip.adapter - -import com.google.gson.Gson -import com.intellij.database.dataSource.DatabaseConnectionCore -import com.intellij.database.datagrid.DataRequest - -class DataGripQueryAdapter( - private val queryScript: String, - private val resultClass: Class, - private val gson: Gson, - ownerEx: OwnerEx, - private val continuation: (List) -> Unit, -): DataRequest.RawRequest(ownerEx) { - override fun processRaw(p0: Context?, p1: DatabaseConnectionCore?) { - val remoteConnection = p1!!.remoteConnection - val statement = remoteConnection.prepareStatement(queryScript.trimIndent()) - - val listOfResults = mutableListOf() - val resultSet = statement.executeQuery() - - if (resultClass.isPrimitive || resultClass == String::class.java) { - while (resultSet.next()) { - listOfResults.add(resultSet.getObject(1) as T) - } - } else { - while (resultSet.next()) { - val hashMap = resultSet.getObject(1) as Map - val result = gson.fromJson(gson.toJson(hashMap), resultClass) - listOfResults.add(result) - } - } - - continuation(listOfResults) - } -} \ No newline at end of file diff --git a/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataSourceQuery.kt b/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataSourceQuery.kt deleted file mode 100644 index becfefa0..00000000 --- a/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataSourceQuery.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.mongodb.jbplugin.accessadapter.datagrip.adapter - -import com.google.gson.Gson -import com.intellij.database.console.session.DatabaseSession -import com.intellij.database.console.session.DatabaseSessionManager -import com.intellij.database.dataSource.LocalDataSource -import com.intellij.openapi.project.Project -import kotlinx.coroutines.* -import java.util.concurrent.TimeoutException -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.coroutines.resume -import kotlin.reflect.KClass -import kotlin.time.Duration - -class DataSourceQuery( - private val project: Project, - private val dataSource: LocalDataSource, - private val result: KClass -) { - private val gson = Gson() - - suspend fun runQuery(queryString: String, timeout: Duration): List = withContext(Dispatchers.IO) { - suspendCancellableCoroutine { callback -> - val session = getSession() - val hasFinished = AtomicBoolean(false) - - launch { - val query = DataGripQueryAdapter(queryString, result.java, gson, session) { - if (!hasFinished.compareAndSet(false, true)) { - callback.resume(it) - } - } - - session.messageBus.dataProducer.processRequest(query) - - delay(timeout) - if (!hasFinished.compareAndSet(false, true)) { - callback.cancel(TimeoutException("Timeout running query '$queryString'")) - } - } - } - } - - private fun getSession(): DatabaseSession { - val sessions = DatabaseSessionManager.getSessions(project, dataSource) - if (sessions.isEmpty()) { - return DatabaseSessionManager.openSession(project, dataSource, "mongodb") - } - - return sessions[0] - } -} \ No newline at end of file diff --git a/packages/mongodb-access-adapter/datagrip-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/IntegrationTest.kt b/packages/mongodb-access-adapter/datagrip-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/IntegrationTest.kt new file mode 100644 index 00000000..05275767 --- /dev/null +++ b/packages/mongodb-access-adapter/datagrip-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/IntegrationTest.kt @@ -0,0 +1,144 @@ +/** + * Test extension that allows us to test with the IntelliJ environment + * without spinning up the whole IDE. Also, sets up a MongoDB instance + * that can be queried. + */ + +package com.mongodb.jbplugin.accessadapter.datagrip + +import com.intellij.database.dataSource.DatabaseDriverManagerImpl +import com.intellij.database.dataSource.LocalDataSource +import com.intellij.database.dataSource.validation.DatabaseDriverValidator.createDownloaderTask +import com.intellij.database.psi.DataSourceManager +import com.intellij.ide.impl.ProjectUtil +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.progress.EmptyProgressIndicator +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager +import com.intellij.testFramework.junit5.RunInEdt +import com.intellij.testFramework.junit5.TestApplication +import com.intellij.util.ui.EDT +import com.mongodb.jbplugin.accessadapter.MongoDbDriver +import com.mongodb.jbplugin.accessadapter.datagrip.adapter.DataGripMongoDbDriver +import org.junit.jupiter.api.extension.* +import org.testcontainers.containers.MongoDBContainer +import org.testcontainers.junit.jupiter.Testcontainers +import org.testcontainers.lifecycle.Startables + +import java.nio.file.Files +import java.util.* + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking + +/** + * Represents what version of MongoDB we support in the plugin. + */ +enum class MongoDbversion(val versionString: String) { + LATEST("7.0.9"), +; +} + +/** + * Annotation to be used in the test, at the class level. + * + * @see com.mongodb.jbplugin.accessadapter.datagrip.adapter.DataGripMongoDbDriverTest + */ +@TestApplication +@RunInEdt(allMethods = true, writeIntent = true) +@ExtendWith(IntegrationTestExtension::class) +@Testcontainers(parallel = false) +annotation class IntegrationTest(val mongodb: MongoDBVersion = MongoDBVersion.LATEST, val sharded: Boolean = false) + +/** + * Extension implementation. Must not be used directly. + */ +internal class IntegrationTestExtension : BeforeAllCallback, + AfterAllCallback, + ParameterResolver { + private val namespace = ExtensionContext.Namespace.create(IntegrationTestExtension::class.java) + private val containerKey = "CONTAINER" + private val projectKey = "PROJECT" + private val driverKey = "DRIVER" + private val versionKey = "VERSION" + + override fun beforeAll(context: ExtensionContext?) { + val annotation = context!!.requiredTestClass.getAnnotation(IntegrationTest::class.java) + val container = MongoDBContainer("mongo:${annotation.mongodb.versionString}-jammy") + .let { + if (annotation.sharded) { + it.withSharding() + } else { + it + } + } + + Startables.deepStart(container).join() + context.getStore(namespace).put(containerKey, container) + context.getStore(namespace).put(versionKey, annotation.mongodb) + + val project = runBlocking(Dispatchers.EDT) { + val testClassName = context.requiredTestClass.simpleName + ProjectUtil.openOrCreateProject(testClassName, Files.createTempDirectory(testClassName))!! + } + + context.getStore(namespace).put(projectKey, project) + + val dataSource = runBlocking { + val dataSourceManager = DataSourceManager.byDataSource(project, LocalDataSource::class.java)!! + val instance = DatabaseDriverManagerImpl.getInstance() + val jdbcDriver = instance.getDriver("mongo") + + val dataSource = LocalDataSource().apply { + name = UUID.randomUUID().toString() + url = container.connectionString + isConfiguredByUrl = true + username = "" + passwordStorage = LocalDataSource.Storage.PERSIST + databaseDriver = jdbcDriver + } + + dataSourceManager.addDataSource(dataSource) + dataSource + } + + createDownloaderTask(dataSource, null).run(EmptyProgressIndicator()) + + runBlocking(Dispatchers.EDT) { + EDT.dispatchAllInvocationEvents() + } + + val driver = DataGripMongoDbDriver(project, dataSource) + context.getStore(namespace).put(driverKey, driver) + + runBlocking(Dispatchers.EDT) { + EDT.dispatchAllInvocationEvents() + } + } + + override fun afterAll(context: ExtensionContext?) { + val project = context!!.getStore(namespace).get(projectKey) as Project + val mongodb = context.getStore(namespace).get(containerKey) as MongoDBContainer + + ApplicationManager.getApplication().invokeLater({ + ProjectManager.getInstance().closeAndDispose(project) + }, ModalityState.defaultModalityState()) + + mongodb.close() + } + + override fun supportsParameter(parameterContext: ParameterContext?, extensionContext: ExtensionContext?): Boolean = + parameterContext?.parameter?.type == Project::class.java || + parameterContext?.parameter?.type == MongoDbDriver::class.java || + parameterContext?.parameter?.type == MongoDBVersion::class.java + + override fun resolveParameter(parameterContext: ParameterContext?, extensionContext: ExtensionContext?): Any = + when (parameterContext?.parameter?.type) { + Project::class.java -> extensionContext!!.getStore(namespace).get(projectKey) + MongoDbDriver::class.java -> extensionContext!!.getStore(namespace).get(driverKey) + MongoDBVersion::class.java -> extensionContext!!.getStore(namespace).get(versionKey) + else -> TODO("Parameter of type ${parameterContext?.parameter?.type?.canonicalName} is not supported.") + } +} \ No newline at end of file diff --git a/packages/mongodb-access-adapter/datagrip-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriverTest.kt b/packages/mongodb-access-adapter/datagrip-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriverTest.kt new file mode 100644 index 00000000..262bf1c6 --- /dev/null +++ b/packages/mongodb-access-adapter/datagrip-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriverTest.kt @@ -0,0 +1,33 @@ +package com.mongodb.jbplugin.accessadapter.datagrip.adapter + +import com.mongodb.jbplugin.accessadapter.MongoDbDriver +import com.mongodb.jbplugin.accessadapter.datagrip.IntegrationTest +import com.mongodb.jbplugin.accessadapter.datagrip.MongoDBVersion +import org.bson.Document +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +import kotlinx.coroutines.runBlocking + +@IntegrationTest +class DataGripMongoDbDriverTest { + @Test + fun `can connect and run a command`(version: MongoDBVersion, driver: MongoDbDriver) = runBlocking { + val result = driver.runCommand(Document(mapOf( + "buildInfo" to 1, + )), Map::class) + + assertEquals(result["version"], version.versionString) + } + + @Test + fun `is able to map the result to a class`(version: MongoDBVersion, driver: MongoDbDriver) = runBlocking { + data class MyBuildInfo(val version: String) + + val result = driver.runCommand(Document(mapOf( + "buildInfo" to 1, + )), MyBuildInfo::class) + + assertEquals(result.version, version.versionString) + } +} \ No newline at end of file diff --git a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDBDriver.kt b/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDBDriver.kt deleted file mode 100644 index 6f1dbdbb..00000000 --- a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDBDriver.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.mongodb.jbplugin.accessadapter - -import org.bson.Document -import org.owasp.encoder.Encode -import kotlin.reflect.KClass -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds - -data class Namespace(val database: String, val collection: String) { - override fun toString(): String { - return "${database}.${collection}" - } -} - -fun String.toNS(): Namespace { - val (db, coll) = trim().split(".", limit = 2) - return Namespace( - Encode.forJavaScript(db), - Encode.forJavaScript(coll) - ) -} - -interface MongoDBDriver { - suspend fun runCommand(command: Document, result: KClass, timeout: Duration = 1.seconds): T - suspend fun findOne(namespace: Namespace, query: Document, options: Document, result: KClass, timeout: Duration = 1.seconds): T? - suspend fun findAll(namespace: Namespace, query: Document, result: KClass, limit: Int = 10, timeout: Duration = 1.seconds): List -} \ No newline at end of file diff --git a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDBReadModelProvider.kt b/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDBReadModelProvider.kt deleted file mode 100644 index 66c89004..00000000 --- a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDBReadModelProvider.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.mongodb.jbplugin.accessadapter - -interface Slice { - suspend fun queryUsingDriver(from: MongoDBDriver): State -} - -interface MongoDBReadModelProvider { - fun slice(dataSource: DataSource, slice: Slice): T -} \ No newline at end of file diff --git a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriver.kt b/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriver.kt new file mode 100644 index 00000000..7b0adf3c --- /dev/null +++ b/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriver.kt @@ -0,0 +1,69 @@ +/** + * Represents the MongoDB Driver facade that we will use internally. + * Usually, we won't use this class directly, only in tests. What we + * will use is the MongoDBReadModelProvider, that provides caching + * and safety mechanisms. + * + * @see com.mongodb.jbplugin.accessadapter.MongoDbReadModelProvider + */ + +package com.mongodb.jbplugin.accessadapter + +import org.bson.Document +import org.owasp.encoder.Encode +import kotlin.reflect.KClass +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +/** + * Represents a MongoDB Namespace (db/coll) + * + * @property database + * @property collection + */ +data class Namespace(val database: String, val collection: String) { + override fun toString(): String = "$database.$collection" +} + +/** + * Represents the MongoDB Driver facade that we will use internally. + * Usually, we won't use this class directly, only in tests. What we + * will use is the MongoDBReadModelProvider, that provides caching + * and safety mechanisms. + * + * @see com.mongodb.jbplugin.accessadapter.MongoDbReadModelProvider + */ +interface MongoDbDriver { + suspend fun runCommand( +command: Document, + result: KClass, + timeout: Duration = 1.seconds +): T + suspend fun findOne( +namespace: Namespace, + query: Document, + options: Document, + result: KClass, + timeout: Duration = 1.seconds +): T? + suspend fun findAll( +namespace: Namespace, + query: Document, + result: KClass, + limit: Int = 10, + timeout: Duration = 1.seconds +): List +} + +/** + * Converts a string in form of `db.coll` to a Namespace object. + * + * @return + */ +fun String.toNs(): Namespace { + val (db, coll) = trim().split(".", limit = 2) + return Namespace( + Encode.forJavaScript(db), + Encode.forJavaScript(coll) + ) +} \ No newline at end of file diff --git a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbReadModelProvider.kt b/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbReadModelProvider.kt new file mode 100644 index 00000000..95671aef --- /dev/null +++ b/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbReadModelProvider.kt @@ -0,0 +1,28 @@ +/** + * Interface that needs to be implemented to access MongoDB. Implementation + * classes must be thread safe. + */ + +package com.mongodb.jbplugin.accessadapter + +/** + * A slice of data from MongoDB. S is the resulting type of the query. + * + * @see com.mongodb.jbplugin.accessadapter.slice.BuildInfo.Slice + * + * @param S + */ +interface Slice { + suspend fun queryUsingDriver(from: MongoDbDriver): S +} + +/** + * Accessing MongoDB state will be done through the provider, that will ensure + * efficient access or caching if necessary. The type `D` is the type of DataSource + * that will be used by the slice. It's an opaque type. + * + * @param D + */ +interface MongoDbReadModelProvider { + fun slice(dataSource: D, slice: Slice): T +} \ No newline at end of file diff --git a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfo.kt b/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfo.kt new file mode 100644 index 00000000..7574aabd --- /dev/null +++ b/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfo.kt @@ -0,0 +1,30 @@ +/** + * A slice that represents the build information of the connected cluster. + */ + +package com.mongodb.jbplugin.accessadapter.slice + +import com.mongodb.jbplugin.accessadapter.MongoDbDriver +import org.bson.Document + +/** + * Slice to be used when querying the MongoDbReadModelProvider. + * + * @see com.mongodb.jbplugin.accessadapter.slice.BuildInfo.Slice + * @see com.mongodb.jbplugin.accessadapter.MongoDbReadModelProvider.slice + * @property version + * @property gitVersion + */ +data class BuildInfo( + val version: String, + val gitVersion: String +) { + object Slice : com.mongodb.jbplugin.accessadapter.Slice { + override suspend fun queryUsingDriver(from: MongoDbDriver): BuildInfo = from.runCommand( + Document(mapOf( + "buildInfo" to 1, + )), + BuildInfo::class + ) + } +} \ No newline at end of file diff --git a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfoSlice.kt b/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfoSlice.kt deleted file mode 100644 index 8cf750b0..00000000 --- a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfoSlice.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.mongodb.jbplugin.accessadapter.slice - -import com.mongodb.jbplugin.accessadapter.MongoDBDriver -import com.mongodb.jbplugin.accessadapter.Slice -import org.bson.Document - -data class BuildInfo( - val version: String, - val gitVersion: String -) -data object BuildInfoSlice : Slice { - override suspend fun queryUsingDriver(from: MongoDBDriver): BuildInfo { - return from.runCommand( - Document(mapOf( - "buildInfo" to 1, - )), - BuildInfo::class - ) - } -} \ No newline at end of file diff --git a/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/MongoDBDriverTest.kt b/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriverTest.kt similarity index 98% rename from packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/MongoDBDriverTest.kt rename to packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriverTest.kt index c9514eb3..9b3e8c1b 100644 --- a/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/MongoDBDriverTest.kt +++ b/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriverTest.kt @@ -3,7 +3,7 @@ package com.mongodb.jbplugin.accessadapter import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test -class MongoDBDriverTest { +class MongoDbDriverTest { @Test fun `parses a namespace`() { val namespace = "mydb.mycoll".toNS() diff --git a/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfoSliceTest.kt b/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfoTest.kt similarity index 74% rename from packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfoSliceTest.kt rename to packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfoTest.kt index 978aadea..197f2f84 100644 --- a/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfoSliceTest.kt +++ b/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfoTest.kt @@ -1,20 +1,21 @@ package com.mongodb.jbplugin.accessadapter.slice -import com.mongodb.jbplugin.accessadapter.MongoDBDriver -import kotlinx.coroutines.runBlocking +import com.mongodb.jbplugin.accessadapter.MongoDbDriver import org.bson.Document import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.mockito.Mockito -class BuildInfoSliceTest { +import kotlinx.coroutines.runBlocking + +class BuildInfoTest { @Test fun `returns a valid build info`(): Unit = runBlocking { val command = Document(mapOf("buildInfo" to 1)) - val driver = Mockito.mock() + val driver = Mockito.mock() Mockito.`when`(driver.runCommand(command, BuildInfo::class)).thenReturn(BuildInfo("7.8.0", "1235abc")) - val data = BuildInfoSlice.queryUsingDriver(driver) + val data = BuildInfo.Slice.queryUsingDriver(driver) Assertions.assertEquals("7.8.0", data.version) Assertions.assertEquals("1235abc", data.gitVersion) }