diff --git a/app/server/appsmith-plugins/mssqlPlugin/dependency-reduced-pom.xml b/app/server/appsmith-plugins/mssqlPlugin/dependency-reduced-pom.xml new file mode 100644 index 0000000000..f62f7e0f96 --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/dependency-reduced-pom.xml @@ -0,0 +1,171 @@ + + + 4.0.0 + com.external.plugins + mssqlPlugin + mssqlPlugin + 1.0-SNAPSHOT + + + + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + false + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + maven-dependency-plugin + + + copy-dependencies + package + + copy-dependencies + + + runtime + ${project.build.directory}/lib + + + + + + + + + org.pf4j + pf4j-spring + 0.6.0 + provided + + + com.appsmith + interfaces + 1.0-SNAPSHOT + provided + + + org.projectlombok + lombok + 1.18.8 + provided + + + junit + junit + 4.13.1 + test + + + hamcrest-core + org.hamcrest + + + + + org.testcontainers + testcontainers + 1.15.0-rc2 + test + + + commons-compress + org.apache.commons + + + duct-tape + org.rnorth.duct-tape + + + visible-assertions + org.rnorth.visible-assertions + + + docker-java-api + com.github.docker-java + + + docker-java-transport-zerodep + com.github.docker-java + + + + + org.testcontainers + mssqlserver + 1.15.0-rc2 + test + + + jdbc + org.testcontainers + + + + + io.projectreactor + reactor-test + 3.2.11.RELEASE + test + + + org.mockito + mockito-core + 3.1.0 + test + + + byte-buddy + net.bytebuddy + + + byte-buddy-agent + net.bytebuddy + + + objenesis + org.objenesis + + + + + + mssql-plugin + ${java.version} + 11 + ${java.version} + com.external.plugins.MssqlPlugin + UTF-8 + 1.0-SNAPSHOT + tech@appsmith.com + + diff --git a/app/server/appsmith-plugins/mssqlPlugin/plugin.properties b/app/server/appsmith-plugins/mssqlPlugin/plugin.properties new file mode 100644 index 0000000000..571e1d3e4d --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/plugin.properties @@ -0,0 +1,5 @@ +plugin.id=mssql-plugin +plugin.class=com.external.plugins.MssqlPlugin +plugin.version=1.0-SNAPSHOT +plugin.provider=tech@appsmith.com +plugin.dependencies= diff --git a/app/server/appsmith-plugins/mssqlPlugin/pom.xml b/app/server/appsmith-plugins/mssqlPlugin/pom.xml new file mode 100644 index 0000000000..fedeaa4eb7 --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/pom.xml @@ -0,0 +1,146 @@ + + + + 4.0.0 + + com.external.plugins + mssqlPlugin + 1.0-SNAPSHOT + + mssqlPlugin + + + UTF-8 + 11 + ${java.version} + ${java.version} + mssql-plugin + com.external.plugins.MssqlPlugin + 1.0-SNAPSHOT + tech@appsmith.com + + + + + + + org.pf4j + pf4j-spring + 0.6.0 + provided + + + + com.appsmith + interfaces + 1.0-SNAPSHOT + provided + + + + org.projectlombok + lombok + 1.18.8 + provided + + + + com.microsoft.sqlserver + mssql-jdbc + 8.4.1.jre11 + + + + + junit + junit + 4.13.1 + test + + + + org.testcontainers + testcontainers + 1.15.0-rc2 + test + + + org.testcontainers + mssqlserver + 1.15.0-rc2 + test + + + + io.projectreactor + reactor-test + 3.2.11.RELEASE + test + + + org.mockito + mockito-core + 3.1.0 + test + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + false + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + + + maven-dependency-plugin + + + copy-dependencies + package + + copy-dependencies + + + runtime + ${project.build.directory}/lib + + + + + + + + diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java new file mode 100644 index 0000000000..ea9f4ecd3b --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java @@ -0,0 +1,307 @@ +package com.external.plugins; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.DatasourceTestResult; +import com.appsmith.external.models.Endpoint; +import com.appsmith.external.models.SSLDetails; +import com.appsmith.external.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.pluginExceptions.StaleConnectionException; +import com.appsmith.external.plugins.BasePlugin; +import com.appsmith.external.plugins.PluginExecutor; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.ObjectUtils; +import org.pf4j.Extension; +import org.pf4j.PluginWrapper; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.appsmith.external.models.Connection.Mode.READ_ONLY; + +public class MssqlPlugin extends BasePlugin { + + private static final String JDBC_DRIVER = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; + + private static final int VALIDITY_CHECK_TIMEOUT = 5; + + private static final String DATE_COLUMN_TYPE_NAME = "date"; + + public MssqlPlugin(PluginWrapper wrapper) { + super(wrapper); + } + + /** + * MsSQL plugin receives the query as json of the following format : + */ + + @Slf4j + @Extension + public static class MssqlPluginExecutor implements PluginExecutor { + + @Override + public Mono execute(Connection connection, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { + + try { + if (connection == null || connection.isClosed() || !connection.isValid(VALIDITY_CHECK_TIMEOUT)) { + log.info("Encountered stale connection in MsSQL plugin. Reporting back."); + throw new StaleConnectionException(); + } + } catch (SQLException error) { + // This exception is thrown only when the timeout to `isValid` is negative. Since, that's not the case, + // here, this should never happen. + log.error("Error checking validity of MsSQL connection.", error); + } + + String query = actionConfiguration.getBody(); + + if (query == null) { + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Missing required parameter: Query.")); + } + + List> rowsList = new ArrayList<>(50); + + Statement statement = null; + ResultSet resultSet = null; + try { + statement = connection.createStatement(); + boolean isResultSet = statement.execute(query); + + if (isResultSet) { + resultSet = statement.getResultSet(); + ResultSetMetaData metaData = resultSet.getMetaData(); + int colCount = metaData.getColumnCount(); + + while (resultSet.next()) { + // Use `LinkedHashMap` here so that the column ordering is preserved in the response. + Map row = new LinkedHashMap<>(colCount); + + for (int i = 1; i <= colCount; i++) { + Object value; + final String typeName = metaData.getColumnTypeName(i); + + if (resultSet.getObject(i) == null) { + value = null; + + } else if (DATE_COLUMN_TYPE_NAME.equalsIgnoreCase(typeName)) { + value = DateTimeFormatter.ISO_DATE.format(resultSet.getDate(i).toLocalDate()); + + } else if ("timestamp".equalsIgnoreCase(typeName)) { + value = DateTimeFormatter.ISO_DATE_TIME.format( + LocalDateTime.of( + resultSet.getDate(i).toLocalDate(), + resultSet.getTime(i).toLocalTime() + ) + ) + "Z"; + + } else if ("timestamptz".equalsIgnoreCase(typeName)) { + value = DateTimeFormatter.ISO_DATE_TIME.format( + resultSet.getObject(i, OffsetDateTime.class) + ); + + } else if ("time".equalsIgnoreCase(typeName) || "timetz".equalsIgnoreCase(typeName)) { + value = resultSet.getString(i); + + } else if ("interval".equalsIgnoreCase(typeName)) { + value = resultSet.getObject(i).toString(); + + } else { + value = resultSet.getObject(i); + + } + + row.put(metaData.getColumnName(i), value); + } + + rowsList.add(row); + } + + } else { + rowsList.add(Map.of( + "affectedRows", + ObjectUtils.defaultIfNull(statement.getUpdateCount(), 0)) + ); + + } + + } catch (SQLException e) { + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getMessage())); + + } finally { + if (resultSet != null) { + try { + resultSet.close(); + } catch (SQLException e) { + log.warn("Error closing MsSQL ResultSet", e); + } + } + + if (statement != null) { + try { + statement.close(); + } catch (SQLException e) { + log.warn("Error closing MsSQL Statement", e); + } + } + + } + + ActionExecutionResult result = new ActionExecutionResult(); + result.setBody(objectMapper.valueToTree(rowsList)); + result.setIsExecutionSuccess(true); + log.debug("In the MssqlPlugin, got action execution result: " + result.toString()); + return Mono.just(result); + } + + @Override + public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { + try { + Class.forName(JDBC_DRIVER); + } catch (ClassNotFoundException e) { + return Mono.error(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Error loading MsSQL JDBC Driver class." + )); + } + + AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + + com.appsmith.external.models.Connection configurationConnection = datasourceConfiguration.getConnection(); + + final boolean isSslEnabled = configurationConnection != null + && configurationConnection.getSsl() != null + && !SSLDetails.AuthType.NO_SSL.equals(configurationConnection.getSsl().getAuthType()); + + StringBuilder urlBuilder = new StringBuilder("jdbc:sqlserver://"); + for (Endpoint endpoint : datasourceConfiguration.getEndpoints()) { + urlBuilder + .append(endpoint.getHost()) + .append(":") + .append(ObjectUtils.defaultIfNull(endpoint.getPort(), 5432L)) + .append(";"); + } + + if (!StringUtils.isEmpty(authentication.getDatabaseName())) { + urlBuilder + .append("database=") + .append(authentication.getDatabaseName()) + .append(";"); + } + + if (!StringUtils.isEmpty(authentication.getUsername())) { + urlBuilder + .append("user=") + .append(authentication.getUsername()) + .append(";"); + } + + if (!StringUtils.isEmpty(authentication.getPassword())) { + urlBuilder + .append("password=") + .append(authentication.getPassword()) + .append(";"); + } + + urlBuilder + .append("encrypt=") + .append(isSslEnabled) + .append(";"); + + try { + Connection connection = DriverManager.getConnection(urlBuilder.toString()); + connection.setReadOnly( + configurationConnection != null && READ_ONLY.equals(configurationConnection.getMode())); + return Mono.just(connection); + + } catch (SQLException e) { + return Mono.error(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Error connecting to MsSQL: " + e.getMessage() + )); + + } + } + + @Override + public void datasourceDestroy(Connection connection) { + try { + if (connection != null) { + connection.close(); + } + } catch (SQLException e) { + log.error("Error closing MsSQL Connection.", e); + } + } + + @Override + public Set validateDatasource(@NonNull DatasourceConfiguration datasourceConfiguration) { + Set invalids = new HashSet<>(); + + if (CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { + invalids.add("Missing endpoint."); + } + + if (datasourceConfiguration.getConnection() != null + && datasourceConfiguration.getConnection().getMode() == null) { + invalids.add("Missing Connection Mode."); + } + + if (datasourceConfiguration.getAuthentication() == null) { + invalids.add("Missing authentication details."); + + } else { + if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getUsername())) { + invalids.add("Missing username for authentication."); + } + + if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getPassword())) { + invalids.add("Missing password for authentication."); + } + + } + + return invalids; + } + + @Override + public Mono testDatasource(DatasourceConfiguration datasourceConfiguration) { + return datasourceCreate(datasourceConfiguration) + .map(connection -> { + try { + if (connection != null) { + connection.close(); + } + } catch (SQLException e) { + log.warn("Error closing MsSQL connection that was made for testing.", e); + } + + return new DatasourceTestResult(); + }) + .onErrorResume(error -> Mono.just(new DatasourceTestResult(error.getMessage()))); + } + + } + +} diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/editor.json b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/editor.json new file mode 100644 index 0000000000..7896a10ac6 --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/editor.json @@ -0,0 +1,15 @@ +{ + "editor": [ + { + "sectionName": "", + "id": 1, + "children": [ + { + "label": "", + "configProperty": "actionConfiguration.body", + "controlType": "QUERY_DYNAMIC_TEXT" + } + ] + } + ] +} \ No newline at end of file diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/form.json new file mode 100644 index 0000000000..6df259b8c7 --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/form.json @@ -0,0 +1,154 @@ +{ + "form": [ + { + "sectionName": "Connection", + "id": 1, + "children": [ + { + "label": "Connection Mode", + "configProperty": "datasourceConfiguration.connection.mode", + "controlType": "DROP_DOWN", + "isRequired": true, + "initialValue": "READ_WRITE", + "options": [ + { + "label": "Read Only", + "value": "READ_ONLY" + }, + { + "label": "Read / Write", + "value": "READ_WRITE" + } + ] + }, + { + "sectionName": null, + "children": [ + { + "label": "Host Address", + "configProperty": "datasourceConfiguration.endpoints[*].host", + "controlType": "KEYVALUE_ARRAY", + "validationMessage": "Please enter a valid host", + "validationRegex": "^((?![/:]).)*$" + }, + { + "label": "Port", + "configProperty": "datasourceConfiguration.endpoints[*].port", + "dataType": "NUMBER", + "controlType": "KEYVALUE_ARRAY" + } + ] + }, + { + "label": "Database Name", + "configProperty": "datasourceConfiguration.authentication.databaseName", + "controlType": "INPUT_TEXT", + "placeholderText": "Database name", + "initialValue": "admin" + } + ] + }, + { + "sectionName": "Authentication", + "id": 2, + "children": [ + { + "sectionName": null, + "children": [ + { + "label": "Username", + "configProperty": "datasourceConfiguration.authentication.username", + "controlType": "INPUT_TEXT", + "placeholderText": "Username" + }, + { + "label": "Password", + "configProperty": "datasourceConfiguration.authentication.password", + "dataType": "PASSWORD", + "controlType": "INPUT_TEXT", + "placeholderText": "Password" + } + ] + } + ] + }, + { + "id": 3, + "sectionName": "SSL (optional)", + "children": [ + { + "label": "SSL Mode", + "configProperty": "datasourceConfiguration.connection.ssl.authType", + "controlType": "DROP_DOWN", + "options": [ + { + "label": "No SSL", + "value": "NO_SSL" + }, + { + "label": "Allow", + "value": "ALLOW" + }, + { + "label": "Prefer", + "value": "PREFER" + }, + { + "label": "Require", + "value": "REQUIRE" + }, + { + "label": "Disable", + "value": "DISABLE" + }, + { + "label": "Verify-CA", + "value": "VERIFY_CA" + }, + { + "label": "Verify-Full", + "value": "VERIFY_FULL" + } + ] + }, + { + "sectionName": null, + "children": [ + { + "label": "Key File", + "configProperty": "datasourceConfiguration.connection.ssl.keyFile", + "controlType": "FILE_PICKER" + }, + { + "label": "Certificate", + "configProperty": "datasourceConfiguration.connection.ssl.certificateFile", + "controlType": "FILE_PICKER" + } + ] + }, + { + "sectionName": null, + "children": [ + { + "label": "CA Certificate", + "configProperty": "datasourceConfiguration.connection.ssl.caCertificateFile", + "controlType": "FILE_PICKER" + }, + { + "label": "PEM Certificate", + "configProperty": "datasourceConfiguration.connection.ssl.pemCertificate.file", + "controlType": "FILE_PICKER" + }, + { + "label": "PEM Passphrase", + "configProperty": "datasourceConfiguration.connection.ssl.pemCertificate.password", + "dataType": "PASSWORD", + "controlType": "INPUT_TEXT", + "placeholderText": "PEM Passphrase" + } + ] + } + ] + } + ] +} diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/CREATE.sql b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/CREATE.sql new file mode 100644 index 0000000000..60948f8b63 --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/CREATE.sql @@ -0,0 +1,8 @@ +INSERT INTO users + (name, gender, email) +VALUES + ( + '{{ nameInput.text }}', + '{{ genderDropdown.selectedOptionValue }}', + '{{ nameInput.text }}' + ); diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/DELETE.sql b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/DELETE.sql new file mode 100644 index 0000000000..aad8425f9b --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/DELETE.sql @@ -0,0 +1 @@ +DELETE FROM users WHERE id = '{{ usersTable.selectedRow.id }}'; diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/SELECT.sql b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/SELECT.sql new file mode 100644 index 0000000000..80ef4f2134 --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/SELECT.sql @@ -0,0 +1 @@ +SELECT * FROM users where role = '{{ roleDropdown.selectedOptionValue }}' ORDER BY id LIMIT 10; \ No newline at end of file diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/UPDATE.sql b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/UPDATE.sql new file mode 100644 index 0000000000..4c1b31b32b --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/UPDATE.sql @@ -0,0 +1,3 @@ +UPDATE users + SET status = 'APPROVED' + WHERE id = '{{ usersTable.selectedRow.id }}'; diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java b/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java new file mode 100644 index 0000000000..fa4bcf1365 --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java @@ -0,0 +1,204 @@ +package com.external.plugins; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.Endpoint; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.testcontainers.containers.MSSQLServerContainer; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.LinkedHashMap; +import java.util.List; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for the PostgresPlugin + */ +@Slf4j +public class MssqlPluginTest { + + MssqlPlugin.MssqlPluginExecutor pluginExecutor = new MssqlPlugin.MssqlPluginExecutor(); + + @SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is pseudo-optional. + @ClassRule + public static final MSSQLServerContainer container = + new MSSQLServerContainer<>("mcr.microsoft.com/mssql/server:2017-latest") + .acceptLicense() + .withExposedPorts(1433) + .withPassword("Mssql123"); + + private static String address; + private static Integer port; + private static String username, password; + + @BeforeClass + public static void setUp() throws SQLException { + address = container.getContainerIpAddress(); + port = container.getMappedPort(1433); + username = container.getUsername(); + password = container.getPassword(); + + try (Connection connection = DriverManager.getConnection( + "jdbc:sqlserver://" + address + ":" + port + ";user=" + username + ";password=" + password + )) { + + try (Statement statement = connection.createStatement()) { + statement.execute("CREATE TABLE users (\n" + + " id int identity (1, 1) NOT NULL,\n" + + " username VARCHAR (50) UNIQUE NOT NULL,\n" + + " password VARCHAR (50) NOT NULL,\n" + + " email VARCHAR (355) UNIQUE NOT NULL,\n" + + " spouse_dob DATE,\n" + + " dob DATE NOT NULL,\n" + + " time1 TIME NOT NULL,\n" + + " constraint pk_users_id primary key (id)\n" + + ")"); + + statement.execute("CREATE TABLE possessions (\n" + + " id int identity (1, 1) not null,\n" + + " title VARCHAR (50) NOT NULL,\n" + + " user_id int NOT NULL,\n" + + " constraint pk_possessions_id primary key (id),\n" + + " constraint user_fk foreign key (user_id) references users(id)\n" + + ")"); + } + + try (Statement statement = connection.createStatement()) { + statement.execute("SET identity_insert users ON;"); + } + + try (Statement statement = connection.createStatement()) { + statement.execute( + "INSERT INTO users (id, username, password, email, spouse_dob, dob, time1) VALUES (" + + "1, 'Jack', 'jill', 'jack@exemplars.com', NULL, '2018-12-31'," + + " '18:32:45'" + + ")"); + } + + try (Statement statement = connection.createStatement()) { + statement.execute( + "INSERT INTO users (id, username, password, email, spouse_dob, dob, time1) VALUES (" + + "2, 'Jill', 'jack', 'jill@exemplars.com', NULL, '2019-12-31'," + + " '15:45:30'" + + ")"); + } + + } + } + + private DatasourceConfiguration createDatasourceConfiguration() { + AuthenticationDTO authDTO = new AuthenticationDTO(); + authDTO.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD); + authDTO.setUsername(username); + authDTO.setPassword(password); + + Endpoint endpoint = new Endpoint(); + endpoint.setHost(address); + endpoint.setPort(port.longValue()); + + DatasourceConfiguration dsConfig = new DatasourceConfiguration(); + dsConfig.setAuthentication(authDTO); + dsConfig.setEndpoints(List.of(endpoint)); + return dsConfig; + } + + @Test + public void testConnectPostgresContainer() { + + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + StepVerifier.create(dsConnectionMono) + .assertNext(Assert::assertNotNull) + .verifyComplete(); + } + + @Test + public void testAliasColumnNames() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT id as user_id FROM users WHERE id = 1"); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertArrayEquals( + new String[]{ + "user_id" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray() + ); + }) + .verifyComplete(); + } + + @Test + public void testExecute() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT * FROM users WHERE id = 1"); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertEquals("2018-12-31", node.get("dob").asText()); + assertEquals("18:32:45.0000000", node.get("time1").asText()); + assertTrue(node.get("spouse_dob").isNull()); + + // Check the order of the columns. + assertArrayEquals( + new String[]{ + "id", + "username", + "password", + "email", + "spouse_dob", + "dob", + "time1", + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray() + ); + }) + .verifyComplete(); + } + +} diff --git a/app/server/appsmith-plugins/pom.xml b/app/server/appsmith-plugins/pom.xml index 032f02dd81..3e2cedb946 100644 --- a/app/server/appsmith-plugins/pom.xml +++ b/app/server/appsmith-plugins/pom.xml @@ -22,5 +22,6 @@ elasticSearchPlugin dynamoPlugin redisPlugin + mssqlPlugin \ No newline at end of file diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java index 2c5a106bde..7777d9715c 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java @@ -994,6 +994,26 @@ public class DatabaseChangelog { installPluginToAllOrganizations(mongoTemplate, plugin1.getId()); } + @ChangeSet(order = "030", id = "add-msSql-plugin", author = "") + public void addMsSqlPlugin(MongoTemplate mongoTemplate) { + Plugin plugin1 = new Plugin(); + plugin1.setName("MsSQL"); + plugin1.setType(PluginType.DB); + plugin1.setPackageName("mssql-plugin"); + plugin1.setUiComponent("DbEditorForm"); + plugin1.setResponseType(Plugin.ResponseType.TABLE); + plugin1.setIconLocation("https://s3.us-east-2.amazonaws.com/assets.appsmith.com/MsSQL.jpg"); + plugin1.setDocumentationLink("https://docs.appsmith.com/core-concepts/connecting-to-databases/querying-mssql"); + plugin1.setDefaultInstall(true); + try { + mongoTemplate.insert(plugin1); + } catch (DuplicateKeyException e) { + log.warn(plugin1.getPackageName() + " already present in database."); + } + + installPluginToAllOrganizations(mongoTemplate, plugin1.getId()); + } + private void installPluginToAllOrganizations(MongoTemplate mongoTemplate, String pluginId) { for (Organization organization : mongoTemplate.findAll(Organization.class)) { if (CollectionUtils.isEmpty(organization.getPlugins())) {