Skip to content

Commit

Permalink
#147: Implement connection auto-completion from appsettings.json, u…
Browse files Browse the repository at this point in the history
…ser secrets and SQLite data sources
  • Loading branch information
seclerp committed Mar 14, 2023
1 parent 44d3d81 commit 989a214
Show file tree
Hide file tree
Showing 20 changed files with 392 additions and 18 deletions.
3 changes: 1 addition & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,7 @@ intellij {
version = "${ProductVersion}"
downloadSources = false
instrumentCode = false
// TODO: add plugins
// plugins = ["uml", "com.jetbrains.ChooseRuntime:1.0.9"]
plugins = ["com.intellij.database"]

patchPluginXml {
changeNotes = changelog.get(PluginVersion).toHTML()
Expand Down
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ PublishChannel=default
# Possible values:
# Release: 2021.2.0
# EAP: 2022.2.0-eap04
RiderSdkVersion=2023.1.0-eap04
RiderSdkVersion=2023.1.0-eap08
# Possible values (minor is omitted):
# Release: 2020.2
# Nightly: 2020.3-SNAPSHOT
# EAP: 2020.3-EAP2-SNAPSHOT
ProductVersion=2023.1-EAP4-SNAPSHOT
ProductVersion=2023.1-EAP8-SNAPSHOT

# Kotlin 1.4 will bundle the stdlib dependency by default, causing problems with the version bundled with the IDE
# https://blog.jetbrains.com/kotlin/2020/07/kotlin-1-4-rc-released/#stdlib-default
Expand Down
4 changes: 2 additions & 2 deletions src/dotnet/Plugin.props
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<Project>

<PropertyGroup>
<SdkVersion Condition=" '$(SdkVersion)' == '' ">2023.1.0-eap01</SdkVersion>
<SdkVersion Condition=" '$(SdkVersion)' == '' ">2023.1.0-eap08</SdkVersion>

<Title>Entity Framework Core</Title>
<Description>JetBrains Rider plugin for Entity Framework Core</Description>

<Authors>Andrew Rublyov</Authors>
<Copyright>Copyright $([System.DateTime]::Now.Year) Andrew Rublyov</Copyright>
<Copyright>Copyright $([System.DateTime]::Now.Year) Andrii Rublov</Copyright>
<PackageTags>resharper plugin</PackageTags>

<PackageProjectUrl></PackageProjectUrl>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package me.seclerp.observables.ui.dsl

import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.observable.util.whenTextChanged
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.TextFieldWithBrowseButton
Expand All @@ -15,8 +16,11 @@ import me.seclerp.observables.ObservableProperty
import me.seclerp.rider.plugins.efcore.ui.IconComboBoxRendererAdapter
import me.seclerp.rider.plugins.efcore.ui.items.IconItem
import java.awt.event.ItemEvent
import javax.swing.ComboBoxEditor
import javax.swing.DefaultComboBoxModel
import javax.swing.Icon
import javax.swing.JTextField
import javax.swing.plaf.basic.BasicComboBoxEditor

fun <T : IconItem<*>> Row.iconComboBox(
selectedItemProperty: Observable<T?>,
Expand Down Expand Up @@ -49,6 +53,49 @@ fun <T : IconItem<*>> Row.iconComboBox(
}
}

fun <T : IconItem<TValue>, TValue> Row.editableComboBox(
selectedTextProperty: Observable<String>,
availableItemsProperty: Observable<List<T>>,
itemMapper: (TValue) -> String
): Cell<ComboBox<T>> {
val model = DefaultComboBoxModel<T>()
.apply {
availableItemsProperty.afterChange {
removeAllElements()
addAll(it)
}
}

return comboBox(model, IconComboBoxRendererAdapter())
.applyToComponent {
isEditable = true
editor = object : BasicComboBoxEditor() {
override fun setItem(anObject: Any?) {
val item = anObject as? IconItem<TValue>
if (item == null && anObject is String?)
editor.text = anObject
else if (item != null)
editor.text = itemMapper(item.data)
}

override fun getItem(): Any {
return editor.text
}
}

val editorComponent = editor.editorComponent as JTextField

editorComponent.whenTextChanged {
selectedTextProperty.value = editorComponent.text
}

selectedTextProperty.afterChange {
editorComponent.text = it
}
}
.align(AlignX.FILL)
}

fun Row.textFieldWithCompletion(
property: ObservableProperty<String>,
completions: MutableList<String>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package me.seclerp.rider.plugins.efcore.features.connections

import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.databind.node.TextNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.util.io.isFile
import com.jetbrains.rider.model.RdCustomLocation
import com.jetbrains.rider.model.RdProjectDescriptor
import kotlin.io.path.Path
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.name

@Service
class AppSettingsConnectionProvider : DbConnectionProvider {
companion object {
private val json = jacksonObjectMapper()
fun getInstance(intellijProject: Project) = intellijProject.service<AppSettingsConnectionProvider>()
}

override fun getAvailableConnections(project: RdProjectDescriptor) =
buildList {
val directory = (project.location as RdCustomLocation?)?.customLocation?.let(::Path)?.parent ?: return@buildList
val connectionStrings = directory.listDirectoryEntries("appsettings*.json")
.filter { it.isFile() }
.map { it.name to json.readTree(it.toFile()) }
.mapNotNull { (fileName, json) -> (json.get("ConnectionStrings") as ObjectNode?)?.let { fileName to it } }
.flatMap { (fileName, obj) ->
obj.fieldNames().asSequence().map { connName ->
(obj[connName] as TextNode?)?.let { node -> Triple(fileName, connName, node.textValue()) }
}
}
.filterNotNull()
.map { (fileName, connName, connString) -> DbConnectionInfo(connName, connString, fileName, null) }

addAll(connectionStrings)
}.toList()
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package me.seclerp.rider.plugins.efcore.features.connections

import com.intellij.database.Dbms
import com.intellij.database.dataSource.LocalDataSource
import com.intellij.database.dataSource.LocalDataSourceManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.jetbrains.rider.model.RdProjectDescriptor

@Service
class DataGripConnectionProvider(private val intellijProject: Project) : DbConnectionProvider {
companion object {
fun getInstance(intellijProject: Project) = intellijProject.service<DataGripConnectionProvider>()
}
override fun getAvailableConnections(project: RdProjectDescriptor) = buildList {
LocalDataSourceManager.getInstance(intellijProject).dataSources.forEach {
val connString = generateConnectionString(it)
if (connString != null)
add(DbConnectionInfo(it.name, connString, "Data sources", it.dbms))
}
}

private fun generateConnectionString(source: LocalDataSource) =
when (source.dbms) {
Dbms.SQLITE -> "Data Source=${source.url?.removePrefix("jdbc:sqlite:")}"
else -> null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package me.seclerp.rider.plugins.efcore.features.connections

import com.intellij.database.Dbms

data class DbConnectionInfo(
val name: String,
val connectionString: String,
val sourceName: String,
val dbms: Dbms?
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.seclerp.rider.plugins.efcore.features.connections

import com.jetbrains.rider.model.RdProjectDescriptor

interface DbConnectionProvider {
fun getAvailableConnections(project: RdProjectDescriptor): List<DbConnectionInfo>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package me.seclerp.rider.plugins.efcore.features.connections

import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.workspaceModel.ide.WorkspaceModel
import com.jetbrains.rider.model.RdProjectDescriptor
import com.jetbrains.rider.projectView.workspace.findProjects
import java.util.*

@Service
@Suppress("UnstableApiUsage")
class DbConnectionsCollector(private val intellijProject: Project) {
companion object {
fun getInstance(intellijProject: Project) = intellijProject.service<DbConnectionsCollector>()
}

private val providers = listOf(
AppSettingsConnectionProvider.getInstance(intellijProject),
UserSecretsConnectionProvider.getInstance(intellijProject),
DataGripConnectionProvider.getInstance(intellijProject)
)

fun collect(projectId: UUID): List<DbConnectionInfo> {
val project = WorkspaceModel.getInstance(intellijProject)
.findProjects()
.filter { it.descriptor is RdProjectDescriptor }
.map { it.descriptor as RdProjectDescriptor }
.firstOrNull { it.originalGuid == projectId }
?: return emptyList()

return providers.flatMap { it.getAvailableConnections(project) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package me.seclerp.rider.plugins.efcore.features.connections

import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.databind.node.TextNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.SystemInfo
import com.jetbrains.rider.ideaInterop.welcomeWizard.transferSettingsRider.utilities.WindowsEnvVariables
import com.jetbrains.rider.model.RdProjectDescriptor
import com.jetbrains.rider.projectView.nodes.getUserData
import kotlin.io.path.Path

@Service
class UserSecretsConnectionProvider : DbConnectionProvider {
companion object {
private val json = jacksonObjectMapper()
private val userSecretsFolder = if (SystemInfo.isWindows)
Path(WindowsEnvVariables.applicationData, "Microsoft", "UserSecrets")
else
Path(System.getenv("HOME"), ".microsoft", "usersecrets")
fun getInstance(intellijProject: Project) = intellijProject.service<UserSecretsConnectionProvider>()
}

override fun getAvailableConnections(project: RdProjectDescriptor) =
buildList {
val userSecretsId = project.getUserData("UserSecretsId") ?: return@buildList
val userSecretsFile = userSecretsFolder.resolve(userSecretsId).resolve("secrets.json").toFile()
if (!userSecretsFile.exists() || !userSecretsFile.isFile)
return@buildList
val obj = json.readTree(userSecretsFile).get("ConnectionStrings") as ObjectNode? ?: return@buildList
obj.fieldNames().forEach { connName ->
val connString = (obj[connName] as TextNode?)?.textValue()
if (connString != null)
add(DbConnectionInfo(connName, connString, "User secrets", null))
}
}.toList()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import com.intellij.openapi.project.Project
import me.seclerp.observables.bind
import me.seclerp.observables.observable
import me.seclerp.observables.observableList
import me.seclerp.rider.plugins.efcore.features.shared.ObservableConnections
import me.seclerp.rider.plugins.efcore.features.shared.ObservableMigrations
import me.seclerp.rider.plugins.efcore.features.shared.dialog.CommonDataContext
import me.seclerp.rider.plugins.efcore.state.DialogsStateService

class UpdateDatabaseDataContext(intellijProject: Project): CommonDataContext(intellijProject, true) {
val observableMigrations = ObservableMigrations(intellijProject, migrationsProject, dbContext)
val availableMigrationNames = observableList<String>()
val observableConnections = ObservableConnections(intellijProject, startupProject)

val migrationNames = observableList<String>()
.apply {
Expand All @@ -29,6 +31,7 @@ class UpdateDatabaseDataContext(intellijProject: Project): CommonDataContext(int
super.initBindings()

observableMigrations.initBinding()
observableConnections.initBinding()

availableMigrationNames.bind(migrationNames) {
buildList {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import me.seclerp.observables.bind
import me.seclerp.observables.observable
import me.seclerp.observables.observableList
import me.seclerp.observables.ui.dsl.bindSelected
import me.seclerp.observables.ui.dsl.bindText
import me.seclerp.observables.ui.dsl.editableComboBox
import me.seclerp.observables.ui.dsl.iconComboBox
import me.seclerp.rider.plugins.efcore.cli.api.DatabaseCommandFactory
import me.seclerp.rider.plugins.efcore.cli.api.models.DotnetEfVersion
import me.seclerp.rider.plugins.efcore.features.connections.DbConnectionInfo
import me.seclerp.rider.plugins.efcore.features.shared.dialog.CommonDialogWrapper
import me.seclerp.rider.plugins.efcore.ui.DbConnectionItemRenderer
import me.seclerp.rider.plugins.efcore.ui.items.DbConnectionItem
import me.seclerp.rider.plugins.efcore.ui.items.MigrationItem
import java.util.*

Expand All @@ -37,8 +40,8 @@ class UpdateDatabaseDialogWrapper(
//
// Internal data
private val targetMigrationsView = observableList<MigrationItem?>()

private val targetMigrationView = observable<MigrationItem?>(null)
private val availableDbConnectionsView = observableList<DbConnectionItem>()

//
// Validation
Expand All @@ -62,6 +65,10 @@ class UpdateDatabaseDialogWrapper(
targetMigrationView.bind(dataCtx.targetMigration,
mappings.migration.toItem,
mappings.migration.fromItem)

availableDbConnectionsView.bind(dataCtx.observableConnections) {
it.map(mappings.dbConnection.toItem)
}
}

override fun generateCommand(): GeneralCommandLine {
Expand Down Expand Up @@ -96,8 +103,8 @@ class UpdateDatabaseDialogWrapper(
.component
}
row("Connection:") {
textField()
.bindText(dataCtx.connection)
editableComboBox(dataCtx.connection, availableDbConnectionsView) { it.connectionString }
.applyToComponent { renderer = DbConnectionItemRenderer() }
.validationOnInput(validator.connectionValidation())
.validationOnApply(validator.connectionValidation())
.enabledIf(useDefaultConnectionCheckbox!!.selected.not())
Expand All @@ -117,6 +124,16 @@ class UpdateDatabaseDialogWrapper(
val fromItem: (MigrationItem?) -> String?
get() = { it?.data }
}

object dbConnection {
val toItem: (DbConnectionInfo) -> DbConnectionItem
get() = {
DbConnectionItem(it)
}

val fromItem: (DbConnectionItem) -> DbConnectionInfo
get() = { it.data }
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.components.JBTextField
import com.intellij.ui.layout.ValidationInfoBuilder
import com.intellij.util.textCompletion.TextFieldWithCompletion
import me.seclerp.rider.plugins.efcore.ui.items.DbConnectionItem
import me.seclerp.rider.plugins.efcore.ui.items.MigrationItem
import javax.swing.text.JTextComponent

class UpdateDatabaseValidator(
private val currentDbContextMigrationsList: MutableList<MigrationItem?>
Expand All @@ -17,8 +19,8 @@ class UpdateDatabaseValidator(
null
}

fun connectionValidation(): ValidationInfoBuilder.(JBTextField) -> ValidationInfo? = {
if (it.isEnabled && it.text.isEmpty())
fun connectionValidation(): ValidationInfoBuilder.(ComboBox<DbConnectionItem>) -> ValidationInfo? = {
if (it.isEnabled && (it.editor.editorComponent as JTextComponent).text.isEmpty())
error("Connection could not be empty")
else null
}
Expand Down
Loading

0 comments on commit 989a214

Please sign in to comment.