Adding microsoft SQL server plugin integration (#1374)

This commit is contained in:
Arpit Mohan 2020-10-23 20:36:45 +05:30 committed by GitHub
parent e9c40da8fd
commit c6d4902e3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1036 additions and 0 deletions

View File

@ -0,0 +1,171 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.external.plugins</groupId>
<artifactId>mssqlPlugin</artifactId>
<name>mssqlPlugin</name>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<minimizeJar>false</minimizeJar>
<transformers>
<transformer>
<manifestEntries>
<Plugin-Id>${plugin.id}</Plugin-Id>
<Plugin-Class>${plugin.class}</Plugin-Class>
<Plugin-Version>${plugin.version}</Plugin-Version>
<Plugin-Provider>${plugin.provider}</Plugin-Provider>
</manifestEntries>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</plugin>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<includeScope>runtime</includeScope>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j-spring</artifactId>
<version>0.6.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.appsmith</groupId>
<artifactId>interfaces</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>hamcrest-core</artifactId>
<groupId>org.hamcrest</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.15.0-rc2</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>commons-compress</artifactId>
<groupId>org.apache.commons</groupId>
</exclusion>
<exclusion>
<artifactId>duct-tape</artifactId>
<groupId>org.rnorth.duct-tape</groupId>
</exclusion>
<exclusion>
<artifactId>visible-assertions</artifactId>
<groupId>org.rnorth.visible-assertions</groupId>
</exclusion>
<exclusion>
<artifactId>docker-java-api</artifactId>
<groupId>com.github.docker-java</groupId>
</exclusion>
<exclusion>
<artifactId>docker-java-transport-zerodep</artifactId>
<groupId>com.github.docker-java</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mssqlserver</artifactId>
<version>1.15.0-rc2</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>jdbc</artifactId>
<groupId>org.testcontainers</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<version>3.2.11.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.1.0</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>byte-buddy</artifactId>
<groupId>net.bytebuddy</groupId>
</exclusion>
<exclusion>
<artifactId>byte-buddy-agent</artifactId>
<groupId>net.bytebuddy</groupId>
</exclusion>
<exclusion>
<artifactId>objenesis</artifactId>
<groupId>org.objenesis</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<properties>
<plugin.id>mssql-plugin</plugin.id>
<maven.compiler.target>${java.version}</maven.compiler.target>
<java.version>11</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<plugin.class>com.external.plugins.MssqlPlugin</plugin.class>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<plugin.version>1.0-SNAPSHOT</plugin.version>
<plugin.provider>tech@appsmith.com</plugin.provider>
</properties>
</project>

View File

@ -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=

View File

@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.external.plugins</groupId>
<artifactId>mssqlPlugin</artifactId>
<version>1.0-SNAPSHOT</version>
<name>mssqlPlugin</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<plugin.id>mssql-plugin</plugin.id>
<plugin.class>com.external.plugins.MssqlPlugin</plugin.class>
<plugin.version>1.0-SNAPSHOT</plugin.version>
<plugin.provider>tech@appsmith.com</plugin.provider>
<plugin.dependencies/>
</properties>
<dependencies>
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j-spring</artifactId>
<version>0.6.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.appsmith</groupId>
<artifactId>interfaces</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>8.4.1.jre11</version>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.15.0-rc2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mssqlserver</artifactId>
<version>1.15.0-rc2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<version>3.2.11.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.1.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<configuration>
<minimizeJar>false</minimizeJar>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Plugin-Id>${plugin.id}</Plugin-Id>
<Plugin-Class>${plugin.class}</Plugin-Class>
<Plugin-Version>${plugin.version}</Plugin-Version>
<Plugin-Provider>${plugin.provider}</Plugin-Provider>
</manifestEntries>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<includeScope>runtime</includeScope>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -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<Connection> {
@Override
public Mono<ActionExecutionResult> 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<Map<String, Object>> 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<String, Object> 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<Connection> 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<String> validateDatasource(@NonNull DatasourceConfiguration datasourceConfiguration) {
Set<String> 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<DatasourceTestResult> 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())));
}
}
}

View File

@ -0,0 +1,15 @@
{
"editor": [
{
"sectionName": "",
"id": 1,
"children": [
{
"label": "",
"configProperty": "actionConfiguration.body",
"controlType": "QUERY_DYNAMIC_TEXT"
}
]
}
]
}

View File

@ -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"
}
]
}
]
}
]
}

View File

@ -0,0 +1,8 @@
INSERT INTO users
(name, gender, email)
VALUES
(
'{{ nameInput.text }}',
'{{ genderDropdown.selectedOptionValue }}',
'{{ nameInput.text }}'
);

View File

@ -0,0 +1 @@
DELETE FROM users WHERE id = '{{ usersTable.selectedRow.id }}';

View File

@ -0,0 +1 @@
SELECT * FROM users where role = '{{ roleDropdown.selectedOptionValue }}' ORDER BY id LIMIT 10;

View File

@ -0,0 +1,3 @@
UPDATE users
SET status = 'APPROVED'
WHERE id = '{{ usersTable.selectedRow.id }}';

View File

@ -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<Connection> dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig);
StepVerifier.create(dsConnectionMono)
.assertNext(Assert::assertNotNull)
.verifyComplete();
}
@Test
public void testAliasColumnNames() {
DatasourceConfiguration dsConfig = createDatasourceConfiguration();
Mono<Connection> dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig);
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setBody("SELECT id as user_id FROM users WHERE id = 1");
Mono<ActionExecutionResult> 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<Connection> dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig);
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setBody("SELECT * FROM users WHERE id = 1");
Mono<ActionExecutionResult> 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();
}
}

View File

@ -22,5 +22,6 @@
<module>elasticSearchPlugin</module>
<module>dynamoPlugin</module>
<module>redisPlugin</module>
<module>mssqlPlugin</module>
</modules>
</project>

View File

@ -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())) {