From 67d0b0393abea24cce099e21cd41b9559560b3d7 Mon Sep 17 00:00:00 2001 From: Arpit Mohan Date: Tue, 21 Jul 2020 12:29:24 +0530 Subject: [PATCH 1/4] Updating the import statement in Cypress test (#129) --- .../Smoke_TestSuite/ApiPaneTests/API_CurlPOSTImport_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_CurlPOSTImport_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_CurlPOSTImport_spec.js index 2bb8ef2cb1..6c6f9189b1 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_CurlPOSTImport_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_CurlPOSTImport_spec.js @@ -1,5 +1,5 @@ const ApiEditor = require("../../../locators/ApiEditor.json"); -const apiwidget = require("../../../locators/apiWidgetsLocator.json"); +const apiwidget = require("../../../locators/apiWidgetslocator.json"); describe("Test curl import flow", function() { it("Test curl import flow for POST action", function() { From af65e1ca9f85b780f0a120b3e8cd6f8469b6a222 Mon Sep 17 00:00:00 2001 From: Hetu Nandu Date: Tue, 21 Jul 2020 12:29:49 +0530 Subject: [PATCH 2/4] Add property checks in evaluated widget (#95) --- .../src/selectors/propertyPaneSelectors.tsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/client/src/selectors/propertyPaneSelectors.tsx b/app/client/src/selectors/propertyPaneSelectors.tsx index bb47cb022f..05350d5e4f 100644 --- a/app/client/src/selectors/propertyPaneSelectors.tsx +++ b/app/client/src/selectors/propertyPaneSelectors.tsx @@ -47,14 +47,18 @@ export const getWidgetPropsForPropertyPane = createSelector( const evaluatedWidget = _.find(evaluatedTree, { widgetId: widget.widgetId, }) as DataTreeWidget; - const widgetProperties = { - ...widget, - evaluatedValues: { ...evaluatedWidget.evaluatedValues }, - }; - if (evaluatedWidget.invalidProps) { - const { invalidProps, validationMessages } = evaluatedWidget; - widgetProperties.invalidProps = invalidProps; - widgetProperties.validationMessages = validationMessages; + const widgetProperties = { ...widget }; + if (evaluatedWidget) { + if (evaluatedWidget.evaluatedValues) { + widgetProperties.evaluatedValues = { + ...evaluatedWidget.evaluatedValues, + }; + } + if (evaluatedWidget.invalidProps) { + const { invalidProps, validationMessages } = evaluatedWidget; + widgetProperties.invalidProps = invalidProps; + widgetProperties.validationMessages = validationMessages; + } } return widgetProperties; }, From 1fc582af085cb9c531687817749bb1aadaa36b4f Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Tue, 21 Jul 2020 13:39:07 +0530 Subject: [PATCH 3/4] When cloning examples organization, clone only public applications (#125) * When cloning examples organization, clone only public applications * Create template organization within the test * Cleaned up test for cloning of examples organization * Fix Mono chaning Co-authored-by: Trisha Anand * Create test apps and config simultaneously Co-authored-by: Trisha Anand --- .../repositories/ApplicationRepository.java | 2 +- .../solutions/ExamplesOrganizationCloner.java | 2 +- .../ExamplesOrganizationClonerTests.java | 64 ++++++++++++------- 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ApplicationRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ApplicationRepository.java index fd49df20f5..47b03288f2 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ApplicationRepository.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ApplicationRepository.java @@ -7,6 +7,6 @@ import reactor.core.publisher.Flux; @Repository public interface ApplicationRepository extends BaseRepository, CustomApplicationRepository { - Flux findByOrganizationId(String organizationId); + Flux findByOrganizationIdAndIsPublicTrue(String organizationId); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java index 2a6ab6ff0f..feea752f8d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java @@ -138,7 +138,7 @@ public class ExamplesOrganizationCloner { private Mono cloneApplications(String fromOrganizationId, String toOrganizationId) { final Mono> cloneDatasourcesMono = cloneDatasources(fromOrganizationId, toOrganizationId).cache(); return applicationRepository - .findByOrganizationId(fromOrganizationId) + .findByOrganizationIdAndIsPublicTrue(fromOrganizationId) .flatMap(application -> { final String templateApplicationId = application.getId(); makePristine(application); diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java index 9ffda333f6..73f2001cb8 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java @@ -1,15 +1,15 @@ package com.appsmith.server.solutions; -import com.appsmith.server.acl.AclPermission; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Config; import com.appsmith.server.domains.Datasource; import com.appsmith.server.domains.Organization; import com.appsmith.server.repositories.ConfigRepository; -import com.appsmith.server.repositories.OrganizationRepository; +import com.appsmith.server.services.ApplicationPageService; import com.appsmith.server.services.ApplicationService; import com.appsmith.server.services.DatasourceService; +import com.appsmith.server.services.OrganizationService; import com.appsmith.server.services.UserService; import lombok.extern.slf4j.Slf4j; import net.minidev.json.JSONObject; @@ -44,9 +44,6 @@ public class ExamplesOrganizationClonerTests { @Autowired private ExamplesOrganizationCloner examplesOrganizationCloner; - @Autowired - private OrganizationRepository organizationRepository; - @Autowired private ConfigRepository configRepository; @@ -56,22 +53,45 @@ public class ExamplesOrganizationClonerTests { @Autowired private DatasourceService datasourceService; + @Autowired + private OrganizationService organizationService; + + @Autowired + private ApplicationPageService applicationPageService; + @Test @WithUserDetails(value = "api_user") - public void createNewUserValid() { - final Mono organizationMono = organizationRepository - .findByName("Spring Test Organization", AclPermission.READ_ORGANIZATIONS) - .flatMap(organization -> { - if (organization.getId() == null) { - log.error("Cannot find Spring Test Organization"); - } - Config config = new Config(); - config.setName(ExamplesOrganizationCloner.TEMPLATE_ORGANIZATION_CONFIG_NAME); - config.setConfig(new JSONObject(Map.of(FieldName.ORGANIZATION_ID, organization.getId()))); - return configRepository.save(config).thenReturn(organization); - }) - .flatMap(organization -> examplesOrganizationCloner.cloneExamplesOrganization()) - .cache(); + public void cloneOrganizationWithItsContents() { + + Organization newOrganization = new Organization(); + newOrganization.setName("Template Organization"); + final Mono organizationMono = organizationService.create(newOrganization) + .flatMap(organization -> { + if (organization.getId() == null) { + return Mono.error(new RuntimeException("Created templates organization doesn't have an ID.")); + } + + Application app1 = new Application(); + app1.setName("1 - public app"); + app1.setOrganizationId(organization.getId()); + app1.setIsPublic(true); + + Application app2 = new Application(); + app2.setOrganizationId(organization.getId()); + app2.setName("2 - private app"); + + Config config = new Config(); + config.setName(ExamplesOrganizationCloner.TEMPLATE_ORGANIZATION_CONFIG_NAME); + config.setConfig(new JSONObject(Map.of(FieldName.ORGANIZATION_ID, organization.getId()))); + + return Mono.when( + applicationPageService.createApplication(app1), + applicationPageService.createApplication(app2), + configRepository.save(config).thenReturn(organization) + ).thenReturn(organization); + }) + .flatMap(organization -> examplesOrganizationCloner.cloneExamplesOrganization()) + .cache(); final Mono, List>> resultMono = Mono.zip( organizationMono, @@ -90,12 +110,10 @@ public class ExamplesOrganizationClonerTests { assertThat(organization.getPolicies()).isNotEmpty(); final List applications = tuple.getT2(); - assertThat(applications).hasSize(3); + assertThat(applications).hasSize(1); assertThat(applications.stream().map(Application::getName).collect(Collectors.toSet())) .containsExactlyInAnyOrder( - "LayoutServiceTest TestApplications", - "TestApplications", - "Another TestApplications" + "1 - public app" ); final List datasources = tuple.getT3(); From d0e60a1890823e6030f8eb0091382ecbd88806b8 Mon Sep 17 00:00:00 2001 From: nupur <35266222+geek-nupur@users.noreply.github.com> Date: Tue, 21 Jul 2020 16:01:42 +0530 Subject: [PATCH 4/4] Mysql plugin integration (#53) This commit adds the Mysql plugin to the Appsmith server. We also add a migration to ensure that this plugin is installed by default for all existing organizations. The migration also adds the plugin details into the DB. Also adding the test cases for mysql plugin. Co-authored-by: Arpit Mohan Co-authored-by: Hetu Nandu Co-authored-by: Arpit Mohan Co-authored-by: Nupur Singhal --- .../mysqlPlugin/plugin.properties | 5 + .../appsmith-plugins/mysqlPlugin/pom.xml | 121 ++++++++++ .../com/external/plugins/MySqlPlugin.java | 219 ++++++++++++++++++ .../mysqlPlugin/src/main/resources/form.json | 152 ++++++++++++ .../src/main/resources/templates/CREATE.sql | 4 + .../src/main/resources/templates/DELETE.sql | 1 + .../src/main/resources/templates/SELECT.sql | 1 + .../src/main/resources/templates/UPDATE.sql | 3 + .../com/external/plugins/MySqlPluginTest.java | 143 ++++++++++++ app/server/appsmith-plugins/pom.xml | 1 + .../postgresPlugin/plugin.properties | 2 +- .../server/migrations/DatabaseChangelog.java | 31 +++ app/server/scripts/start-dev-server.sh | 2 +- 13 files changed, 683 insertions(+), 2 deletions(-) create mode 100644 app/server/appsmith-plugins/mysqlPlugin/plugin.properties create mode 100644 app/server/appsmith-plugins/mysqlPlugin/pom.xml create mode 100644 app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java create mode 100644 app/server/appsmith-plugins/mysqlPlugin/src/main/resources/form.json create mode 100644 app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/CREATE.sql create mode 100644 app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/DELETE.sql create mode 100644 app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/SELECT.sql create mode 100644 app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/UPDATE.sql create mode 100644 app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java diff --git a/app/server/appsmith-plugins/mysqlPlugin/plugin.properties b/app/server/appsmith-plugins/mysqlPlugin/plugin.properties new file mode 100644 index 0000000000..812b11576c --- /dev/null +++ b/app/server/appsmith-plugins/mysqlPlugin/plugin.properties @@ -0,0 +1,5 @@ +plugin.id=mysql-plugin +plugin.class=com.external.plugins.MysqlPlugin +plugin.version=1.0-SNAPSHOT +plugin.provider=tech@appsmith.com +plugin.dependencies= \ No newline at end of file diff --git a/app/server/appsmith-plugins/mysqlPlugin/pom.xml b/app/server/appsmith-plugins/mysqlPlugin/pom.xml new file mode 100644 index 0000000000..a3c1ddb834 --- /dev/null +++ b/app/server/appsmith-plugins/mysqlPlugin/pom.xml @@ -0,0 +1,121 @@ + + + + 4.0.0 + + com.external.plugins + mysqlPlugin + 1.0-SNAPSHOT + + mysqlPlugin + + + UTF-8 + 11 + ${java.version} + ${java.version} + mysql-plugin + com.external.plugins.MySqlPlugin + 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 + + + + mysql + mysql-connector-java + 8.0.20 + runtime + + + + + junit + junit + 4.11 + test + + + + org.testcontainers + testcontainers + 1.13.0 + test + + + org.testcontainers + mysql + 1.14.1 + 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} + + + + + + + package + + shade + + + + + + + + diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java new file mode 100644 index 0000000000..8194b6cad8 --- /dev/null +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java @@ -0,0 +1,219 @@ +package com.external.plugins; + +import com.appsmith.external.models.*; +import com.appsmith.external.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.plugins.BasePlugin; +import com.appsmith.external.plugins.PluginExecutor; +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.*; +import java.sql.Connection; +import java.util.*; + +import static com.appsmith.external.models.Connection.Mode.READ_ONLY; + +public class MySqlPlugin extends BasePlugin { + + static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver"; + + private static final String USER = "user"; + private static final String PASSWORD = "password"; + + public MySqlPlugin(PluginWrapper wrapper) { + super(wrapper); + } + + @Slf4j + @Extension + public static class MySqlPluginExecutor implements PluginExecutor { + + private Mono pluginErrorMono(Object... args) { + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, args)); + } + + @Override + public Mono execute(Object connection, DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { + + Connection conn = (Connection) connection; + + String query = actionConfiguration.getBody(); + + if (query == null) { + return pluginErrorMono("Missing required parameter: Query."); + } + + List> rowsList = new ArrayList<>(50); + + Statement statement = null; + ResultSet resultSet = null; + try { + statement = conn.createStatement(); + boolean isResultSet = statement.execute(query); + + if (isResultSet) { + resultSet = statement.getResultSet(); + ResultSetMetaData metaData = resultSet.getMetaData(); + int colCount = metaData.getColumnCount(); + while (resultSet.next()) { + Map row = new HashMap<>(colCount); + for (int i = 1; i <= colCount; i++) { + row.put(metaData.getColumnName(i), resultSet.getObject(i)); + } + rowsList.add(row); + } + + } else { + rowsList.add(Map.of("affectedRows", statement.getUpdateCount())); + + } + + } catch (SQLException e) { + return pluginErrorMono(e.getMessage()); + + } finally { + if (resultSet != null) { + try { + resultSet.close(); + } catch (SQLException e) { + log.warn("Error closing MySql ResultSet", e); + } + } + + if (statement != null) { + try { + statement.close(); + } catch (SQLException e) { + log.warn("Error closing MySql Statement", e); + } + } + + } + + ActionExecutionResult result = new ActionExecutionResult(); + result.setBody(objectMapper.valueToTree(rowsList)); + result.setIsExecutionSuccess(true); + log.debug("In the MySqlPlugin, 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 pluginErrorMono("Error loading MySql JDBC Driver class."); + } + + String url; + AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + + com.appsmith.external.models.Connection configurationConnection = datasourceConfiguration.getConnection(); + + Properties properties = new Properties(); + properties.putAll(Map.of( + USER, authentication.getUsername(), + PASSWORD, authentication.getPassword() + // TODO: Set SSL connection parameters. + )); + + if (CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { + url = datasourceConfiguration.getUrl(); + + } else { + StringBuilder urlBuilder = new StringBuilder("jdbc:mysql://"); + for (Endpoint endpoint : datasourceConfiguration.getEndpoints()) { + urlBuilder + .append(endpoint.getHost()) + .append(':') + .append(ObjectUtils.defaultIfNull(endpoint.getPort(), 3306L)) + .append('/'); + + if (!StringUtils.isEmpty(authentication.getDatabaseName())) { + urlBuilder.append(authentication.getDatabaseName()); + } + } + url = urlBuilder.toString(); + } + + try { + Connection connection = DriverManager.getConnection(url, properties); + connection.setReadOnly( + configurationConnection != null && READ_ONLY.equals(configurationConnection.getMode())); + return Mono.just(connection); + } catch (SQLException e) { + return pluginErrorMono("Error connecting to MySql.", e); + } + } + + @Override + public void datasourceDestroy(Object connection) { + Connection conn = (Connection) connection; + try { + if (conn != null) { + conn.close(); + } + } catch (SQLException e) { + log.error("Error closing MySQL Connection.", e); + } + } + + @Override + public Set validateDatasource(DatasourceConfiguration datasourceConfiguration) { + + Set invalids = new HashSet<>(); + + if (datasourceConfiguration.getConnection() != null + && datasourceConfiguration.getConnection().getMode() == null) { + invalids.add("Missing Connection Mode."); + } + + if (StringUtils.isEmpty(datasourceConfiguration.getUrl()) && + CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { + invalids.add("Missing endpoint and url"); + } + + 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."); + } + + if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getDatabaseName())) { + invalids.add("Missing database name"); + } + } + + return invalids; + } + + @Override + public Mono testDatasource(DatasourceConfiguration datasourceConfiguration) { + return datasourceCreate(datasourceConfiguration) + .map(connection -> { + try { + if (connection != null) { + ((Connection) connection).close(); + } + } catch (SQLException e) { + log.warn("Error closing MySQL 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/mysqlPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/form.json new file mode 100644 index 0000000000..65765dd23f --- /dev/null +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/form.json @@ -0,0 +1,152 @@ +{ + "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" + }, + { + "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/mysqlPlugin/src/main/resources/templates/CREATE.sql b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/CREATE.sql new file mode 100644 index 0000000000..37d328b85c --- /dev/null +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/CREATE.sql @@ -0,0 +1,4 @@ +INSERT INTO users + (id, name, gender, avatar, email, address, role) +VALUES + (?, ?, ?, ?, ?, ?, ?); diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/DELETE.sql b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/DELETE.sql new file mode 100644 index 0000000000..c487578842 --- /dev/null +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/DELETE.sql @@ -0,0 +1 @@ +DELETE FROM users WHERE id = ?; diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/SELECT.sql b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/SELECT.sql new file mode 100644 index 0000000000..0a8208db0d --- /dev/null +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/SELECT.sql @@ -0,0 +1 @@ +SELECT * FROM users ORDER BY id LIMIT 10; diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/UPDATE.sql b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/UPDATE.sql new file mode 100644 index 0000000000..18445fd6c3 --- /dev/null +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/UPDATE.sql @@ -0,0 +1,3 @@ +UPDATE users + SET status = 'APPROVED' + WHERE id = 1; diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java new file mode 100644 index 0000000000..6e143e56e8 --- /dev/null +++ b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java @@ -0,0 +1,143 @@ +package com.external.plugins; + +import com.appsmith.external.models.*; +import lombok.extern.log4j.Log4j; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.testcontainers.containers.MySQLContainer; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.*; + +@Log4j +public class MySqlPluginTest { + + MySqlPlugin.MySqlPluginExecutor pluginExecutor = new MySqlPlugin.MySqlPluginExecutor(); + + @ClassRule + public static MySQLContainer mySQLContainer = new MySQLContainer() + .withUsername("mysql").withPassword("password").withDatabaseName("mysql"); + + String address; + Integer port; + String username, password; + + DatasourceConfiguration dsConfig; + + @Before + public void setUp() { + address = mySQLContainer.getContainerIpAddress(); + port = mySQLContainer.getFirstMappedPort(); + username = mySQLContainer.getUsername(); + password = mySQLContainer.getPassword(); + createDatasourceConfiguration(); + } + + private DatasourceConfiguration createDatasourceConfiguration() { + AuthenticationDTO authDTO = new AuthenticationDTO(); + authDTO.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD); + authDTO.setUsername(username); + authDTO.setPassword(password); + authDTO.setDatabaseName("mysql"); + + Endpoint endpoint = new Endpoint(); + endpoint.setHost(address); + endpoint.setPort(port.longValue()); + + dsConfig = new DatasourceConfiguration(); + dsConfig.setAuthentication(authDTO); + dsConfig.setEndpoints(List.of(endpoint)); + return dsConfig; + } + + @Test + public void testConnectMySQLContainer() { + + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + StepVerifier.create(dsConnectionMono) + .assertNext(connection -> { + java.sql.Connection conn = (Connection) connection; + assertNotNull(conn); + }) + .verifyComplete(); + } + + @Test + public void testExecute() { + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("show databases"); + + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + System.out.println(result); + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + }) + .verifyComplete(); + } + + @Test + public void testValidateDatasourceNullCredentials() { + dsConfig.setConnection(new com.appsmith.external.models.Connection()); + dsConfig.getAuthentication().setUsername(null); + dsConfig.getAuthentication().setPassword(null); + dsConfig.getAuthentication().setDatabaseName("someDbName"); + Set output = pluginExecutor.validateDatasource(dsConfig); + assertTrue(output.contains("Missing username for authentication.")); + assertTrue(output.contains("Missing password for authentication.")); + } + + @Test + public void testValidateDatasourceMissingDBName() { + dsConfig.getAuthentication().setDatabaseName(""); + Set output = pluginExecutor.validateDatasource(dsConfig); + assertEquals(output.size(), 1); + assertTrue(output.contains("Missing database name")); + } + + @Test + public void testValidateDatasourceNullEndpoint() { + dsConfig.setEndpoints(null); + Set output = pluginExecutor.validateDatasource(dsConfig); + assertEquals(output.size(), 1); + assertTrue(output.contains("Missing endpoint and url")); + } + + /* checking that the connection is being closed after the datadourceDestroy method is being called + NOT : this test case will fail in case of a SQL Exception + */ + @Test + public void testDatasourceDestroy() { + + Mono connectionMono = pluginExecutor.datasourceCreate(dsConfig); + + StepVerifier.create(connectionMono) + .assertNext(connection -> { + java.sql.Connection conn = (Connection) connection; + pluginExecutor.datasourceDestroy(conn); + try { + assertEquals(conn.isClosed(), true); + } catch (SQLException e) { + e.printStackTrace(); + } + }) + .verifyComplete(); + + } + +} \ No newline at end of file diff --git a/app/server/appsmith-plugins/pom.xml b/app/server/appsmith-plugins/pom.xml index eb1160af2c..7fe77590bd 100644 --- a/app/server/appsmith-plugins/pom.xml +++ b/app/server/appsmith-plugins/pom.xml @@ -19,6 +19,7 @@ restApiPlugin mongoPlugin rapidApiPlugin + mysqlPlugin \ No newline at end of file diff --git a/app/server/appsmith-plugins/postgresPlugin/plugin.properties b/app/server/appsmith-plugins/postgresPlugin/plugin.properties index b397b41281..bfb0b59873 100644 --- a/app/server/appsmith-plugins/postgresPlugin/plugin.properties +++ b/app/server/appsmith-plugins/postgresPlugin/plugin.properties @@ -2,4 +2,4 @@ plugin.id=postgres-plugin plugin.class=com.external.plugins.PostgresPlugin plugin.version=1.0-SNAPSHOT plugin.provider=tech@appsmith.com -plugin.dependencies= +plugin.dependencies= \ 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 41a3d92a08..d50c65d37a 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 @@ -491,4 +491,35 @@ public class DatabaseChangelog { mongoTemplate.save(datasource); } } + + @ChangeSet(order = "018", id = "install-mysql-plugins", author = "") + public void mysqlPlugin(MongoTemplate mongoTemplate) { + Plugin plugin1 = new Plugin(); + plugin1.setName("Mysql"); + plugin1.setType(PluginType.DB); + plugin1.setPackageName("mysql-plugin"); + plugin1.setUiComponent("DbEditorForm"); + plugin1.setIconLocation("https://s3.us-east-2.amazonaws.com/assets.appsmith.com/Mysql.png"); + plugin1.setDefaultInstall(true); + try { + mongoTemplate.insert(plugin1); + } catch (DuplicateKeyException e) { + log.warn("mysql-plugin already present in database."); + } + + for (Organization organization : mongoTemplate.findAll(Organization.class)) { + if (CollectionUtils.isEmpty(organization.getPlugins())) { + organization.setPlugins(new ArrayList<>()); + } + + final Set installedPlugins = organization.getPlugins() + .stream().map(OrganizationPlugin::getPluginId).collect(Collectors.toSet()); + + if (!installedPlugins.contains(plugin1.getId())) { + organization.getPlugins() + .add(new OrganizationPlugin(plugin1.getId(), OrganizationPluginStatus.FREE)); + } + mongoTemplate.save(organization); + } + } } diff --git a/app/server/scripts/start-dev-server.sh b/app/server/scripts/start-dev-server.sh index bf6cd14ed3..9e1ec6324c 100755 --- a/app/server/scripts/start-dev-server.sh +++ b/app/server/scripts/start-dev-server.sh @@ -10,4 +10,4 @@ if [[ -f .env ]]; then source .env fi -exec java -jar dist/server-*.jar +(cd dist && exec java -jar server-*.jar)