From e5574c194589a3a76d3067f79757c7dd2008401d Mon Sep 17 00:00:00 2001 From: Trisha Anand Date: Thu, 18 Feb 2021 18:33:27 +0530 Subject: [PATCH] Support Prepared Statements in Postgres (#2967) * Pushing minor editor form changes to ensure that prepared statement could be turned off. * Code refactor to do variable substitution in PluginExecutor instead of action service. * WIP : Prepared Statement handling in psql plugin * WIP Prepared Statements. * Working version of prepared statements * Quote trimming added for post preparing sql statements. Now the unprepared statements and prepared statements do not require edits. * Fixed existing test cases failing. * Code formatting. * Super minor code cleanup. * Added migration for the existing postgres actions. * Fixed failing test cases in ActionServiceTest. * Minor change in the text for turning on and off prepared statements in the postgres query pane. * Added test cases for prepared statement. * Some minor comments for code readability * Moved Prepared Statement setting from Action Configuration to Plugin Specified Templates since this setting does not make sense for all the DB plugins. * Added function level comments * Update app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SqlStringUtils.java Co-authored-by: Arpit Mohan * Update app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SqlStringUtils.java Co-authored-by: Arpit Mohan * Incorporated review comments. * Fixed compile time error. Co-authored-by: Arpit Mohan --- app/server/appsmith-interfaces/pom.xml | 23 ++ .../appsmith/external/constants/DataType.java | 15 ++ .../external/dtos/ExecuteActionDTO.java | 21 ++ .../external}/helpers/BeanCopyUtils.java | 2 +- .../external}/helpers/MustacheHelper.java | 20 +- .../external/helpers/SqlStringUtils.java | 235 ++++++++++++++++++ .../external/models/ActionConfiguration.java | 1 - .../external/plugins/PluginExecutor.java | 81 +++++- .../external}/helpers/BeanCopyUtilsTest.java | 2 +- .../external}/helpers/MustacheHelperTest.java | 12 +- .../src/main/resources/editor.json | 3 +- .../com/external/plugins/MySqlPlugin.java | 7 +- .../com/external/plugins/PostgresPlugin.java | 197 +++++++++++---- .../src/main/resources/editor.json | 17 ++ .../external/plugins/PostgresPluginTest.java | 217 +++++++++++++++- .../com/external/plugins/RestApiPlugin.java | 72 +++++- .../server/controllers/ActionController.java | 2 +- .../server/dtos/ExecuteActionDTO.java | 30 --- .../server/migrations/DatabaseChangelog.java | 121 ++++++--- .../services/DatasourceServiceImpl.java | 4 +- .../services/LayoutActionServiceImpl.java | 4 +- .../server/services/NewActionService.java | 6 +- .../server/services/NewActionServiceImpl.java | 120 ++------- .../server/services/NewPageServiceImpl.java | 2 +- .../server/services/UserServiceImpl.java | 2 +- .../server/solutions/PageLoadActionsUtil.java | 2 +- .../server/services/ActionServiceTest.java | 14 +- 27 files changed, 977 insertions(+), 255 deletions(-) create mode 100644 app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/DataType.java create mode 100644 app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/ExecuteActionDTO.java rename app/server/{appsmith-server/src/main/java/com/appsmith/server => appsmith-interfaces/src/main/java/com/appsmith/external}/helpers/BeanCopyUtils.java (98%) rename app/server/{appsmith-server/src/main/java/com/appsmith/server => appsmith-interfaces/src/main/java/com/appsmith/external}/helpers/MustacheHelper.java (92%) create mode 100644 app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SqlStringUtils.java rename app/server/{appsmith-server/src/test/java/com/appsmith/server => appsmith-interfaces/src/test/java/com/appsmith/external}/helpers/BeanCopyUtilsTest.java (98%) rename app/server/{appsmith-server/src/test/java/com/appsmith/server => appsmith-interfaces/src/test/java/com/appsmith/external}/helpers/MustacheHelperTest.java (98%) delete mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ExecuteActionDTO.java diff --git a/app/server/appsmith-interfaces/pom.xml b/app/server/appsmith-interfaces/pom.xml index 5b8464b63c..cba3efd203 100644 --- a/app/server/appsmith-interfaces/pom.xml +++ b/app/server/appsmith-interfaces/pom.xml @@ -92,6 +92,29 @@ querydsl-jpa 4.2.2 + + org.apache.commons + commons-text + 1.8 + compile + + + commons-validator + commons-validator + 1.7 + compile + + + commons-io + commons-io + 2.6 + compile + + + org.assertj + assertj-core + test + diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/DataType.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/DataType.java new file mode 100644 index 0000000000..7a813bbc8e --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/DataType.java @@ -0,0 +1,15 @@ +package com.appsmith.external.constants; + +public enum DataType { + INTEGER, + LONG, + FLOAT, + DOUBLE, + BOOLEAN, + DATE, + TIME, + ASCII, + BINARY, + BYTES, + STRING +} diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/ExecuteActionDTO.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/ExecuteActionDTO.java new file mode 100644 index 0000000000..6565a8109c --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/ExecuteActionDTO.java @@ -0,0 +1,21 @@ +package com.appsmith.external.dtos; + +import com.appsmith.external.models.PaginationField; +import com.appsmith.external.models.Param; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class ExecuteActionDTO { + + String actionId; + + List params; + + PaginationField paginationField; + + Boolean viewMode = false; +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/BeanCopyUtils.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/BeanCopyUtils.java similarity index 98% rename from app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/BeanCopyUtils.java rename to app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/BeanCopyUtils.java index 7ca08a737c..6ba6f56949 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/BeanCopyUtils.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/BeanCopyUtils.java @@ -1,4 +1,4 @@ -package com.appsmith.server.helpers; +package com.appsmith.external.helpers; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/MustacheHelper.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/MustacheHelper.java similarity index 92% rename from app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/MustacheHelper.java rename to app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/MustacheHelper.java index a68bd66e68..136f7b4733 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/MustacheHelper.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/MustacheHelper.java @@ -1,4 +1,4 @@ -package com.appsmith.server.helpers; +package com.appsmith.external.helpers; import lombok.extern.slf4j.Slf4j; import org.apache.commons.text.StringEscapeUtils; @@ -18,7 +18,7 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import static com.appsmith.server.helpers.BeanCopyUtils.isDomainModel; +import static com.appsmith.external.helpers.BeanCopyUtils.isDomainModel; @Slf4j public class MustacheHelper { @@ -161,6 +161,22 @@ public class MustacheHelper { return keys; } + // For prepared statements we should extract the bindings in order in a list and include duplicate bindings as well. + public static List extractMustacheKeysInOrder(String template) { + List keys = new ArrayList<>(); + + for (String token : tokenize(template)) { + if (token.startsWith("{{") && token.endsWith("}}")) { + // Allowing empty tokens to be added, to be compatible with the previous `extractMustacheKeys` method. + // Calling `.trim()` before adding because Mustache compiler strips keys in the template before looking + // up a value. Addresses https://www.notion.so/appsmith/Bindings-with-a-space-at-the-start-fail-to-execute-properly-in-the-API-pane-2eb65d5c6064466b9ef059fa01ef3261 + keys.add(token.substring(2, token.length() - 2).trim()); + } + } + + return keys; + } + public static Set extractMustacheKeysFromFields(Object object) { final Set keys = new HashSet<>(); diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SqlStringUtils.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SqlStringUtils.java new file mode 100644 index 0000000000..9ebd467909 --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SqlStringUtils.java @@ -0,0 +1,235 @@ +package com.appsmith.external.helpers; + +import com.appsmith.external.constants.DataType; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.models.ActionConfiguration; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.apache.commons.validator.routines.DateValidator; + +import java.io.UnsupportedEncodingException; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Time; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Slf4j +public class SqlStringUtils { + + /** + * SQL query : The regex pattern below looks for '?' or "?". This pattern is later replaced with ? + * to fit the requirements of prepared statements. + */ + private static String regexQuotesTrimming = "([\"']\\?[\"'])"; + // The final replacement string of ? for replacing '?' or "?" + private static String postQuoteTrimmingQuestionMark = "\\?"; + + private static Pattern quoteQuestionPattern = Pattern.compile(regexQuotesTrimming); + + public static class DateValidatorUsingDateFormat extends DateValidator { + private String dateFormat; + + public DateValidatorUsingDateFormat(String dateFormat) { + this.dateFormat = dateFormat; + } + + @Override + public boolean isValid(String dateStr) { + DateFormat sdf = new SimpleDateFormat(this.dateFormat); + sdf.setLenient(false); + try { + sdf.parse(dateStr); + } catch (ParseException e) { + return false; + } + return true; + } + } + + public static DataType stringToKnownDataTypeConverter(String input) { + + try { + Integer.parseInt(input); + return DataType.INTEGER; + } catch (NumberFormatException e) { + // Not an integer + } + + try { + Long.parseLong(input); + return DataType.LONG; + } catch (NumberFormatException e1) { + // Not long + } + + try { + Float.parseFloat(input); + return DataType.FLOAT; + } catch (NumberFormatException e2) { + // Not float + } + + try { + Double.parseDouble(input); + return DataType.DOUBLE; + } catch (NumberFormatException e3) { + // Not double + } + + // Creating a copy of the input in lower case form to do simple string equality to check for boolean/null types. + String copyInput = String.valueOf(input).toLowerCase().trim(); + if (copyInput.equals("true") || copyInput.equals("false")) { + return DataType.BOOLEAN; + } + + if (copyInput.equals("null")) { + return null; + } + + DateValidator dateValidator = new DateValidatorUsingDateFormat("yyyy-mm-dd"); + if (dateValidator.isValid(input)) { + return DataType.DATE; + } + + DateValidator dateTimeValidator = new DateValidatorUsingDateFormat("yyyy-mm-dd hh:mm:ss"); + if (dateTimeValidator.isValid(input)) { + return DataType.DATE; + } + + DateValidator timeValidator = new DateValidatorUsingDateFormat("hh:mm:ss"); + if (timeValidator.isValid(input)) { + return DataType.TIME; + } + + /** + * TODO : Timestamp, ASCII, Binary and Bytes Array + */ + +// // Check if unicode stream also gets handled as part of this since the destination SQL type is the same. +// if(StandardCharsets.US_ASCII.newEncoder().canEncode(input)) { +// return Ascii.class; +// } +// if (isBinary(input)) { +// return Binary.class; +// } + +// try +// { +// input.getBytes("UTF-8"); +// return Byte.class; +// } catch (UnsupportedEncodingException e) { +// // Not byte +// } + + + // default return type if none of the above matches. + return DataType.STRING; + } + + public static PreparedStatement setValueInPreparedStatement(int index, String binding, String value, PreparedStatement preparedStatement) throws UnsupportedEncodingException, AppsmithPluginException { + DataType valueType = SqlStringUtils.stringToKnownDataTypeConverter(value); + + /** + * TODO : Parse the column name for which the value is null and if the column name exists in the + * database structure, find the column field type and use PreparedStatement.setNull() function. + */ + // If the value being set is null, return without setting. + if (valueType == null) { + return preparedStatement; + } + + try { + switch (valueType) { + case BINARY: { + preparedStatement.setBinaryStream(index, IOUtils.toInputStream(value)); + break; + } + case BYTES: { + preparedStatement.setBytes(index, value.getBytes("UTF-8")); + break; + } + case INTEGER: { + preparedStatement.setInt(index, Integer.parseInt(value)); + break; + } + case LONG: { + preparedStatement.setLong(index, Long.parseLong(value)); + break; + } + case FLOAT: { + preparedStatement.setFloat(index, Float.parseFloat(value)); + break; + } + case DOUBLE: { + preparedStatement.setDouble(index, Double.parseDouble(value)); + break; + } + case BOOLEAN: { + preparedStatement.setBoolean(index, Boolean.parseBoolean(value)); + break; + } + case DATE: { + preparedStatement.setDate(index, Date.valueOf(value)); + break; + } + case TIME: { + preparedStatement.setTime(index, Time.valueOf(value)); + break; + } + case STRING: { + preparedStatement.setString(index, value); + break; + } + default: + break; + } + + } catch (SQLException | IllegalArgumentException e) { + String message = "Query preparation failed while inserting value: " + + value + " for binding: {{" + binding + "}}. Please check the query again.\nError: " + e.getMessage(); + throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, message); + } + + return preparedStatement; + } + + private static boolean isBinary(String input) { + for (int i = 0; i < input.length(); i++) { + int tempB = input.charAt(i); + if (tempB == '0' || tempB == '1') { + continue; + } + return false; + } + // no failures, so + return true; + } + + public static String replaceMustacheWithQuestionMark(String query, List mustacheBindings) { + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody(query); + + Map replaceParamsMap = mustacheBindings + .stream() + .collect(Collectors.toMap(Function.identity(), v -> "?")); + + ActionConfiguration updatedActionConfiguration = MustacheHelper.renderFieldValues(actionConfiguration, replaceParamsMap); + + String body = updatedActionConfiguration.getBody(); + + // Trim the quotes around ? if present + body = quoteQuestionPattern.matcher(body).replaceAll(postQuoteTrimmingQuestionMark); + + return body; + } +} diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionConfiguration.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionConfiguration.java index cc2b828b88..730a293bd6 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionConfiguration.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionConfiguration.java @@ -47,7 +47,6 @@ public class ActionConfiguration { // DB action fields // JS action fields - String jsFunction; /* diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/PluginExecutor.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/PluginExecutor.java index 6e0f4bdbf8..ea5024c003 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/PluginExecutor.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/PluginExecutor.java @@ -1,20 +1,27 @@ package com.appsmith.external.plugins; +import com.appsmith.external.dtos.ExecuteActionDTO; +import com.appsmith.external.helpers.MustacheHelper; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceStructure; import com.appsmith.external.models.DatasourceTestResult; +import com.appsmith.external.models.Param; import org.pf4j.ExtensionPoint; import org.springframework.util.CollectionUtils; import reactor.core.publisher.Mono; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; public interface PluginExecutor extends ExtensionPoint { /** - * This function is used to execute the action. + * This function is implemented by the plugins by default to execute the action. + *

+ * If executeParametrized has a custom implementation by a plugin, this function would not be used. * * @param connection : This is the connection that is established to the data source. This connection is according * to the parameters in Datasource Configuration @@ -56,11 +63,11 @@ public interface PluginExecutor extends ExtensionPoint { * This function checks if the datasource is valid. It should only check if all the mandatory fields are filled and * if the values are of the right format. It does NOT check the validity of those fields. * Please use {@link #testDatasource(DatasourceConfiguration)} to establish the correctness of those fields. - * + *

* If the datasource configuration is valid, it should return an empty set of invalid strings. * If not, it should return the list of invalid messages as a set. * - * @param datasourceConfiguration : The datasource to be validated + * @param datasourceConfiguration : The datasource to be validated * @return Set : The set of invalid strings informing the user of all the invalid fields */ Set validateDatasource(DatasourceConfiguration datasourceConfiguration); @@ -85,4 +92,72 @@ public interface PluginExecutor extends ExtensionPoint { default Mono getStructure(C connection, DatasourceConfiguration datasourceConfiguration) { return Mono.empty(); } + + /** + * Appsmith Server calls this function for execution of the action. + * Default implementation which takes the variables that need to be substituted and then calls the plugin execute function + *

+ * Plugins requiring their custom implementation of variable substitution should override this function and then are + * responsible both for variable substitution and final execution. + * + * @param connection : This is the connection that is established to the data source. This connection is according + * to the parameters in Datasource Configuration + * @param executeActionDTO : This is the data structure sent by the client during execute. This contains the params + * which would be used for substitution + * @param datasourceConfiguration : These are the configurations which have been used to create a Datasource from a Plugin + * @param actionConfiguration : These are the configurations which have been used to create an Action from a Datasource. + * @return ActionExecutionResult : This object is returned to the user which contains the result values from the execution. + */ + default Mono executeParameterized(C connection, + ExecuteActionDTO executeActionDTO, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { + prepareConfigurationsForExecution(executeActionDTO, actionConfiguration, datasourceConfiguration); + return this.execute(connection, datasourceConfiguration, actionConfiguration); + } + + /** + * This function is responsible for preparing the action and datasource configurations to be ready for execution. + * + * @param executeActionDTO + * @param actionConfiguration + * @param datasourceConfiguration + */ + default void prepareConfigurationsForExecution(ExecuteActionDTO executeActionDTO, + ActionConfiguration actionConfiguration, + DatasourceConfiguration datasourceConfiguration) { + + variableSubstitution(actionConfiguration, datasourceConfiguration, executeActionDTO); + + return; + } + + /** + * This function replaces the variables in the action and datasource configuration with the actual params + */ + default void variableSubstitution(ActionConfiguration actionConfiguration, + DatasourceConfiguration datasourceConfiguration, + ExecuteActionDTO executeActionDTO) { + //Do variable substitution + //Do this only if params have been provided in the execute command + if (executeActionDTO.getParams() != null && !executeActionDTO.getParams().isEmpty()) { + Map replaceParamsMap = executeActionDTO + .getParams() + .stream() + .collect(Collectors.toMap( + // Trimming here for good measure. If the keys have space on either side, + // Mustache won't be able to find the key. + // We also add a backslash before every double-quote or backslash character + // because we apply the template replacing in a JSON-stringified version of + // these properties, where these two characters are escaped. + p -> p.getKey().trim(), // .replaceAll("[\"\n\\\\]", "\\\\$0"), + Param::getValue, + // In case of a conflict, we pick the older value + (oldValue, newValue) -> oldValue) + ); + + MustacheHelper.renderFieldValues(datasourceConfiguration, replaceParamsMap); + MustacheHelper.renderFieldValues(actionConfiguration, replaceParamsMap); + } + } } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/BeanCopyUtilsTest.java b/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/BeanCopyUtilsTest.java similarity index 98% rename from app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/BeanCopyUtilsTest.java rename to app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/BeanCopyUtilsTest.java index 9d7e518606..fdc8ea5a64 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/BeanCopyUtilsTest.java +++ b/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/BeanCopyUtilsTest.java @@ -1,4 +1,4 @@ -package com.appsmith.server.helpers; +package com.appsmith.external.helpers; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/MustacheHelperTest.java b/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/MustacheHelperTest.java similarity index 98% rename from app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/MustacheHelperTest.java rename to app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/MustacheHelperTest.java index d9b550a883..536306b6ad 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/MustacheHelperTest.java +++ b/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/MustacheHelperTest.java @@ -1,4 +1,4 @@ -package com.appsmith.server.helpers; +package com.appsmith.external.helpers; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.Connection; @@ -14,11 +14,11 @@ import java.util.List; import java.util.Map; import java.util.Set; -import static com.appsmith.server.helpers.MustacheHelper.extractMustacheKeys; -import static com.appsmith.server.helpers.MustacheHelper.extractMustacheKeysFromFields; -import static com.appsmith.server.helpers.MustacheHelper.render; -import static com.appsmith.server.helpers.MustacheHelper.renderFieldValues; -import static com.appsmith.server.helpers.MustacheHelper.tokenize; +import static com.appsmith.external.helpers.MustacheHelper.extractMustacheKeys; +import static com.appsmith.external.helpers.MustacheHelper.extractMustacheKeysFromFields; +import static com.appsmith.external.helpers.MustacheHelper.render; +import static com.appsmith.external.helpers.MustacheHelper.renderFieldValues; +import static com.appsmith.external.helpers.MustacheHelper.tokenize; import static org.assertj.core.api.Assertions.assertThat; @SuppressWarnings( diff --git a/app/server/appsmith-plugins/amazons3Plugin/src/main/resources/editor.json b/app/server/appsmith-plugins/amazons3Plugin/src/main/resources/editor.json index 7d6fbccb95..caa984eea8 100644 --- a/app/server/appsmith-plugins/amazons3Plugin/src/main/resources/editor.json +++ b/app/server/appsmith-plugins/amazons3Plugin/src/main/resources/editor.json @@ -14,7 +14,8 @@ { "label": "-- Select --", "value": "" - },{ + }, + { "label": "List files in bucket", "value": "LIST" }, 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 index 3f95085eec..ab8bd534d9 100644 --- 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 @@ -1,5 +1,8 @@ package com.external.plugins; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.DBAuth; @@ -8,9 +11,6 @@ import com.appsmith.external.models.DatasourceStructure; import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.models.Endpoint; import com.appsmith.external.models.Property; -import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; -import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; -import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; import com.appsmith.external.plugins.BasePlugin; import com.appsmith.external.plugins.PluginExecutor; import io.r2dbc.spi.ColumnMetadata; @@ -27,7 +27,6 @@ import org.pf4j.Extension; import org.pf4j.PluginWrapper; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; -import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java index 0eba84e546..5a8dfda659 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java @@ -1,8 +1,11 @@ package com.external.plugins; +import com.appsmith.external.dtos.ExecuteActionDTO; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; +import com.appsmith.external.helpers.MustacheHelper; +import com.appsmith.external.helpers.SqlStringUtils; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.DBAuth; @@ -10,6 +13,8 @@ import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceStructure; import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.models.Endpoint; +import com.appsmith.external.models.Param; +import com.appsmith.external.models.Property; import com.appsmith.external.models.SSLDetails; import com.appsmith.external.plugins.BasePlugin; import com.appsmith.external.plugins.PluginExecutor; @@ -26,6 +31,7 @@ import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; @@ -39,10 +45,14 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + public class PostgresPlugin extends BasePlugin { static final String JDBC_DRIVER = "org.postgresql.Driver"; @@ -55,7 +65,7 @@ public class PostgresPlugin extends BasePlugin { private static final int MAXIMUM_POOL_SIZE = 5; - private static final long LEAK_DETECTION_TIME_MS = 60*1000; + private static final long LEAK_DETECTION_TIME_MS = 60 * 1000; public PostgresPlugin(PluginWrapper wrapper) { super(wrapper); @@ -68,61 +78,113 @@ public class PostgresPlugin extends BasePlugin { private static final String TABLES_QUERY = "select a.attname as name,\n" + - " t1.typname as column_type,\n" + - " case when a.atthasdef then pg_get_expr(d.adbin, d.adrelid) end as default_expr,\n" + - " c.relkind as kind,\n" + - " c.relname as table_name,\n" + - " n.nspname as schema_name\n" + - "from pg_catalog.pg_attribute a\n" + - " left join pg_catalog.pg_type t1 on t1.oid = a.atttypid\n" + - " inner join pg_catalog.pg_class c on a.attrelid = c.oid\n" + - " left join pg_catalog.pg_namespace n on c.relnamespace = n.oid\n" + - " left join pg_catalog.pg_attrdef d on d.adrelid = c.oid and d.adnum = a.attnum\n" + - "where a.attnum > 0\n" + - " and not a.attisdropped\n" + - " and n.nspname not in ('information_schema', 'pg_catalog')\n" + - " and c.relkind in ('r', 'v')\n" + - " and pg_catalog.pg_table_is_visible(a.attrelid)\n" + - "order by c.relname, a.attnum;"; + " t1.typname as column_type,\n" + + " case when a.atthasdef then pg_get_expr(d.adbin, d.adrelid) end as default_expr,\n" + + " c.relkind as kind,\n" + + " c.relname as table_name,\n" + + " n.nspname as schema_name\n" + + "from pg_catalog.pg_attribute a\n" + + " left join pg_catalog.pg_type t1 on t1.oid = a.atttypid\n" + + " inner join pg_catalog.pg_class c on a.attrelid = c.oid\n" + + " left join pg_catalog.pg_namespace n on c.relnamespace = n.oid\n" + + " left join pg_catalog.pg_attrdef d on d.adrelid = c.oid and d.adnum = a.attnum\n" + + "where a.attnum > 0\n" + + " and not a.attisdropped\n" + + " and n.nspname not in ('information_schema', 'pg_catalog')\n" + + " and c.relkind in ('r', 'v')\n" + + " and pg_catalog.pg_table_is_visible(a.attrelid)\n" + + "order by c.relname, a.attnum;"; public static final String KEYS_QUERY = "select c.conname as constraint_name,\n" + - " c.contype as constraint_type,\n" + - " sch.nspname as self_schema,\n" + - " tbl.relname as self_table,\n" + - " array_agg(col.attname order by u.attposition) as self_columns,\n" + - " f_sch.nspname as foreign_schema,\n" + - " f_tbl.relname as foreign_table,\n" + - " array_agg(f_col.attname order by f_u.attposition) as foreign_columns,\n" + - " pg_get_constraintdef(c.oid) as definition\n" + - "from pg_constraint c\n" + - " left join lateral unnest(c.conkey) with ordinality as u(attnum, attposition) on true\n" + - " left join lateral unnest(c.confkey) with ordinality as f_u(attnum, attposition)\n" + - " on f_u.attposition = u.attposition\n" + - " join pg_class tbl on tbl.oid = c.conrelid\n" + - " join pg_namespace sch on sch.oid = tbl.relnamespace\n" + - " left join pg_attribute col on (col.attrelid = tbl.oid and col.attnum = u.attnum)\n" + - " left join pg_class f_tbl on f_tbl.oid = c.confrelid\n" + - " left join pg_namespace f_sch on f_sch.oid = f_tbl.relnamespace\n" + - " left join pg_attribute f_col on (f_col.attrelid = f_tbl.oid and f_col.attnum = f_u.attnum)\n" + - "group by constraint_name, constraint_type, self_schema, self_table, definition, foreign_schema, foreign_table\n" + - "order by self_schema, self_table;"; + " c.contype as constraint_type,\n" + + " sch.nspname as self_schema,\n" + + " tbl.relname as self_table,\n" + + " array_agg(col.attname order by u.attposition) as self_columns,\n" + + " f_sch.nspname as foreign_schema,\n" + + " f_tbl.relname as foreign_table,\n" + + " array_agg(f_col.attname order by f_u.attposition) as foreign_columns,\n" + + " pg_get_constraintdef(c.oid) as definition\n" + + "from pg_constraint c\n" + + " left join lateral unnest(c.conkey) with ordinality as u(attnum, attposition) on true\n" + + " left join lateral unnest(c.confkey) with ordinality as f_u(attnum, attposition)\n" + + " on f_u.attposition = u.attposition\n" + + " join pg_class tbl on tbl.oid = c.conrelid\n" + + " join pg_namespace sch on sch.oid = tbl.relnamespace\n" + + " left join pg_attribute col on (col.attrelid = tbl.oid and col.attnum = u.attnum)\n" + + " left join pg_class f_tbl on f_tbl.oid = c.confrelid\n" + + " left join pg_namespace f_sch on f_sch.oid = f_tbl.relnamespace\n" + + " left join pg_attribute f_col on (f_col.attrelid = f_tbl.oid and f_col.attnum = f_u.attnum)\n" + + "group by constraint_name, constraint_type, self_schema, self_table, definition, foreign_schema, foreign_table\n" + + "order by self_schema, self_table;"; + private static final int PREPARED_STATEMENT_INDEX = 0; + + /** + * Instead of using the default executeParametrized provided by pluginExecutor, this implementation affords an opportunity + * to use PreparedStatement (if configured) which requires the variable substitution, etc. to happen in a particular format + * supported by PreparedStatement. In case of PreparedStatement turned off, the action and datasource configurations are + * prepared (binding replacement) using PluginExecutor.variableSubstitution + * + * @param connection : This is the connection that is established to the data source. This connection is according + * to the parameters in Datasource Configuration + * @param executeActionDTO : This is the data structure sent by the client during execute. This contains the params + * which would be used for substitution + * @param datasourceConfiguration : These are the configurations which have been used to create a Datasource from a Plugin + * @param actionConfiguration : These are the configurations which have been used to create an Action from a Datasource. + * @return + */ @Override - public Mono execute(HikariDataSource connection, - DatasourceConfiguration datasourceConfiguration, - ActionConfiguration actionConfiguration) { + public Mono executeParameterized(HikariDataSource connection, + ExecuteActionDTO executeActionDTO, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { + + String query = actionConfiguration.getBody(); + // Check for query parameter before performing the probably expensive fetch connection from the pool op. + if (query == null) { + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "Missing required " + + "parameter: Query.")); + } + + Boolean isPreparedStatement; + + final List properties = actionConfiguration.getPluginSpecifiedTemplates(); + if (properties.get(PREPARED_STATEMENT_INDEX) == null) { + // If the configuration does not exist, default to true + // Note this is not possible today since the query editor sets a default value for this field. + isPreparedStatement = true; + } else { + isPreparedStatement = Boolean.parseBoolean(properties.get(PREPARED_STATEMENT_INDEX).getValue()); + } + + // In case of non prepared statement, simply do binding replacement and execute + if (FALSE.equals(isPreparedStatement)) { + prepareConfigurationsForExecution(executeActionDTO, actionConfiguration, datasourceConfiguration); + return executeCommon(connection, datasourceConfiguration, actionConfiguration, FALSE, null, null); + } + + //Prepared Statement + // First extract all the bindings in order + List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(query); + // Replace all the bindings with a ? as expected in a prepared statement. + String updatedQuery = SqlStringUtils.replaceMustacheWithQuestionMark(query, mustacheKeysInOrder); + actionConfiguration.setBody(updatedQuery); + return executeCommon(connection, datasourceConfiguration, actionConfiguration, TRUE, mustacheKeysInOrder, executeActionDTO); + } + + private Mono executeCommon(HikariDataSource connection, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration, + Boolean preparedStatement, + List mustacheValuesInOrder, + ExecuteActionDTO executeActionDTO) { return Mono.fromCallable(() -> { String query = actionConfiguration.getBody(); - // Check for query parameter before performing the probably expensive fetch connection from the pool op. - if (query == null) { - return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "Missing required " + - "parameter: Query.")); - } - Connection connectionFromPool = null; + Connection connectionFromPool; try { connectionFromPool = getConnectionFromConnectionPool(connection, datasourceConfiguration); @@ -138,6 +200,7 @@ public class PostgresPlugin extends BasePlugin { Statement statement = null; ResultSet resultSet = null; + boolean isResultSet; HikariPoolMXBean poolProxy = connection.getHikariPoolMXBean(); @@ -150,13 +213,33 @@ public class PostgresPlugin extends BasePlugin { "] Hikari Pool stats : active - " + activeConnections + ", idle - " + idleConnections + ", awaiting - " + threadsAwaitingConnection + - ", total - " + totalConnections ); + ", total - " + totalConnections); try { - statement = connectionFromPool.createStatement(); - boolean isResultSet = statement.execute(query); + if (FALSE.equals(preparedStatement)) { + statement = connectionFromPool.createStatement(); + isResultSet = statement.execute(query); + resultSet = statement.getResultSet(); + } else { + PreparedStatement preparedQuery = connectionFromPool.prepareStatement(query); + if (mustacheValuesInOrder != null && !mustacheValuesInOrder.isEmpty()) { + List params = executeActionDTO.getParams(); + for (int i = 0; i < mustacheValuesInOrder.size(); i++) { + String key = mustacheValuesInOrder.get(i); + Optional matchingParam = params.stream().filter(param -> param.getKey().trim().equals(key)).findFirst(); + if (matchingParam.isPresent()) { + String value = matchingParam.get().getValue(); + preparedQuery = SqlStringUtils.setValueInPreparedStatement(i + 1, key, + value, preparedQuery); + } + } + } + System.out.println("Prepared query is : " + preparedQuery.toString()); + isResultSet = preparedQuery.execute(); + resultSet = preparedQuery.getResultSet(); + } if (isResultSet) { - resultSet = statement.getResultSet(); + ResultSetMetaData metaData = resultSet.getMetaData(); int colCount = metaData.getColumnCount(); @@ -226,7 +309,7 @@ public class PostgresPlugin extends BasePlugin { System.out.println(Thread.currentThread().getName() + ": After executing postgres query, Hikari Pool stats active - " + activeConnections + ", idle - " + idleConnections + ", awaiting - " + threadsAwaitingConnection + - ", total - " + totalConnections ); + ", total - " + totalConnections); if (resultSet != null) { try { resultSet.close(); @@ -272,6 +355,12 @@ public class PostgresPlugin extends BasePlugin { } + @Override + public Mono execute(HikariDataSource connection, DatasourceConfiguration datasourceConfiguration, ActionConfiguration actionConfiguration) { + // Unused function + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Unsupported Operation")); + } + @Override public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { try { @@ -380,7 +469,7 @@ public class PostgresPlugin extends BasePlugin { " Hikari Pool stats : active - " + activeConnections + ", idle - " + idleConnections + ", awaiting - " + threadsAwaitingConnection + - ", total - " + totalConnections ); + ", total - " + totalConnections); // Ref: . try (Statement statement = connectionFromPool.createStatement()) { @@ -523,7 +612,7 @@ public class PostgresPlugin extends BasePlugin { System.out.println(Thread.currentThread().getName() + ": After postgres db structure, Hikari Pool stats active - " + activeConnections + ", idle - " + idleConnections + ", awaiting - " + threadsAwaitingConnection + - ", total - " + totalConnections ); + ", total - " + totalConnections); if (connectionFromPool != null) { try { @@ -550,6 +639,7 @@ public class PostgresPlugin extends BasePlugin { /** * This function is blocking in nature which connects to the database and creates a connection pool + * * @param datasourceConfiguration * @return connection pool */ @@ -613,6 +703,7 @@ public class PostgresPlugin extends BasePlugin { /** * First checks if the connection pool is still valid. If yes, we fetch a connection from the pool and return * In case a connection is not available in the pool, SQL Exception is thrown + * * @param connectionPool * @return SQL Connection */ diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/resources/editor.json b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/editor.json index 7896a10ac6..8385c19dee 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/main/resources/editor.json +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/editor.json @@ -4,6 +4,23 @@ "sectionName": "", "id": 1, "children": [ + { + "label": "Use Prepared Statement", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[0].value", + "controlType": "DROP_DOWN", + "isRequired": true, + "initialValue": "true", + "options": [ + { + "label": "Turn on Prepared Statement to prevent SQL injections", + "value": "true" + }, + { + "label": "Turn off Prepared Statement to configure dynamic setting of column names using bindings", + "value": "false" + } + ] + }, { "label": "", "configProperty": "actionConfiguration.body", diff --git a/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java b/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java index ba47697fb6..60dc7e5028 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java +++ b/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java @@ -1,5 +1,6 @@ package com.external.plugins; +import com.appsmith.external.dtos.ExecuteActionDTO; import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; @@ -7,6 +8,8 @@ import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceStructure; import com.appsmith.external.models.Endpoint; +import com.appsmith.external.models.Param; +import com.appsmith.external.models.Property; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -208,9 +211,12 @@ public class PostgresPluginTest { ActionConfiguration actionConfiguration = new ActionConfiguration(); actionConfiguration.setBody("SELECT id as user_id FROM users WHERE id = 1"); + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "false")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration)); + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); StepVerifier.create(executeMono) .assertNext(result -> { @@ -236,8 +242,12 @@ public class PostgresPluginTest { ActionConfiguration actionConfiguration = new ActionConfiguration(); actionConfiguration.setBody("SELECT * FROM users WHERE id = 1"); + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "false")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration)); + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); StepVerifier.create(executeMono) .assertNext(result -> { @@ -405,16 +415,217 @@ public class PostgresPluginTest { ActionConfiguration actionConfiguration = new ActionConfiguration(); actionConfiguration.setBody("show databases"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "false")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + Mono connectionCreateMono = pluginExecutor.datasourceCreate(dsConfig); Mono resultMono = connectionCreateMono .flatMap(pool -> { pool.close(); - return pluginExecutor.execute(pool, dsConfig, actionConfiguration); + return pluginExecutor.executeParameterized(pool, new ExecuteActionDTO(), dsConfig, actionConfiguration); }); StepVerifier.create(resultMono) .expectErrorMatches(throwable -> throwable instanceof StaleConnectionException) .verify(); } + + @Test + public void testPreparedStatementWithoutQuotes() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + // First test with the binding not surrounded with quotes + actionConfiguration.setBody("SELECT * FROM public.\"users\" where id = {{binding1}};"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param = new Param(); + param.setKey("binding1"); + param.setValue("1"); + params.add(param); + executeActionDTO.setParams(params); + + Mono connectionCreateMono = pluginExecutor.datasourceCreate(dsConfig).cache(); + + Mono resultMono = connectionCreateMono + .flatMap(pool -> pluginExecutor.executeParameterized(pool, executeActionDTO, dsConfig, actionConfiguration)); + + StepVerifier.create(resultMono) + .assertNext(result -> { + + assertTrue(result.getIsExecutionSuccess()); + + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertEquals("2018-12-31", node.get("dob").asText()); + assertEquals("18:32:45", node.get("time1").asText()); + assertEquals("04:05:06-08", node.get("time_tz").asText()); + assertEquals("2018-11-30T20:45:15Z", node.get("created_on").asText()); + assertEquals("2018-11-30T19:45:15Z", node.get("created_on_tz").asText()); + assertEquals("1 years 5 mons 0 days 2 hours 0 mins 0.0 secs", node.get("interval1").asText()); + assertTrue(node.get("spouse_dob").isNull()); + + // Check the order of the columns. + assertArrayEquals( + new String[]{ + "id", + "username", + "password", + "email", + "spouse_dob", + "dob", + "time1", + "time_tz", + "created_on", + "created_on_tz", + "interval1", + "numbers", + "texts", + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray() + ); + + }) + .verifyComplete(); + } + + @Test + public void testPreparedStatementWithDoubleQuotes() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT * FROM public.\"users\" where id = \"{{binding1}}\";"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param = new Param(); + param.setKey("binding1"); + param.setValue("1"); + params.add(param); + executeActionDTO.setParams(params); + + Mono connectionCreateMono = pluginExecutor.datasourceCreate(dsConfig).cache(); + + Mono resultMono = connectionCreateMono + .flatMap(pool -> pluginExecutor.executeParameterized(pool, executeActionDTO, dsConfig, actionConfiguration)); + + StepVerifier.create(resultMono) + .assertNext(result -> { + + assertTrue(result.getIsExecutionSuccess()); + + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertEquals("2018-12-31", node.get("dob").asText()); + assertEquals("18:32:45", node.get("time1").asText()); + assertEquals("04:05:06-08", node.get("time_tz").asText()); + assertEquals("2018-11-30T20:45:15Z", node.get("created_on").asText()); + assertEquals("2018-11-30T19:45:15Z", node.get("created_on_tz").asText()); + assertEquals("1 years 5 mons 0 days 2 hours 0 mins 0.0 secs", node.get("interval1").asText()); + assertTrue(node.get("spouse_dob").isNull()); + + // Check the order of the columns. + assertArrayEquals( + new String[]{ + "id", + "username", + "password", + "email", + "spouse_dob", + "dob", + "time1", + "time_tz", + "created_on", + "created_on_tz", + "interval1", + "numbers", + "texts", + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray() + ); + + }) + .verifyComplete(); + } + + @Test + public void testPreparedStatementWithSingleQuotes() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT * FROM public.\"users\" where id = '{{binding1}}';"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param = new Param(); + param.setKey("binding1"); + param.setValue("1"); + params.add(param); + executeActionDTO.setParams(params); + + Mono connectionCreateMono = pluginExecutor.datasourceCreate(dsConfig).cache(); + + Mono resultMono = connectionCreateMono + .flatMap(pool -> pluginExecutor.executeParameterized(pool, executeActionDTO, dsConfig, actionConfiguration)); + + StepVerifier.create(resultMono) + .assertNext(result -> { + + assertTrue(result.getIsExecutionSuccess()); + + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertEquals("2018-12-31", node.get("dob").asText()); + assertEquals("18:32:45", node.get("time1").asText()); + assertEquals("04:05:06-08", node.get("time_tz").asText()); + assertEquals("2018-11-30T20:45:15Z", node.get("created_on").asText()); + assertEquals("2018-11-30T19:45:15Z", node.get("created_on_tz").asText()); + assertEquals("1 years 5 mons 0 days 2 hours 0 mins 0.0 secs", node.get("interval1").asText()); + assertTrue(node.get("spouse_dob").isNull()); + + // Check the order of the columns. + assertArrayEquals( + new String[]{ + "id", + "username", + "password", + "email", + "spouse_dob", + "dob", + "time1", + "time_tz", + "created_on", + "created_on_tz", + "interval1", + "numbers", + "texts", + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray() + ); + + }) + .verifyComplete(); + } } diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java index ee79165fdf..194fc3605f 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java @@ -1,13 +1,16 @@ package com.external.plugins; +import com.appsmith.external.dtos.ExecuteActionDTO; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionRequest; import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceTestResult; +import com.appsmith.external.models.PaginationField; +import com.appsmith.external.models.PaginationType; import com.appsmith.external.models.Property; -import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; -import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; import com.appsmith.external.plugins.BasePlugin; import com.appsmith.external.plugins.PluginExecutor; import com.external.connections.APIConnection; @@ -45,6 +48,7 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; +import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.Instant; @@ -79,6 +83,45 @@ public class RestApiPlugin extends BasePlugin { private final String SESSION_SIGNATURE_KEY_KEY = "sessionSignatureKey"; private final String SIGNATURE_HEADER_NAME = "X-APPSMITH-SIGNATURE"; + /** + * Instead of using the default executeParametrized provided by pluginExecutor, this implementation affords an opportunity + * also update the datasource and action configuration for pagination and some minor cleanup of the configuration before execution + * + * @param connection : This is the connection that is established to the data source. This connection is according + * to the parameters in Datasource Configuration + * @param executeActionDTO : This is the data structure sent by the client during execute. This contains the params + * which would be used for substitution + * @param datasourceConfiguration : These are the configurations which have been used to create a Datasource from a Plugin + * @param actionConfiguration : These are the configurations which have been used to create an Action from a Datasource. + * @return + */ + @Override + public Mono executeParameterized(APIConnection connection, + ExecuteActionDTO executeActionDTO, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { + + prepareConfigurationsForExecution(executeActionDTO, actionConfiguration, datasourceConfiguration); + + // If the action is paginated, update the configurations to update the correct URL. + if (actionConfiguration != null && + actionConfiguration.getPaginationType() != null && + PaginationType.URL.equals(actionConfiguration.getPaginationType()) && + executeActionDTO.getPaginationField() != null) { + datasourceConfiguration = updateDatasourceConfigurationForPagination(actionConfiguration, datasourceConfiguration, executeActionDTO.getPaginationField()); + actionConfiguration = updateActionConfigurationForPagination(actionConfiguration, executeActionDTO.getPaginationField()); + } + // Filter out any empty headers + if (actionConfiguration.getHeaders() != null && !actionConfiguration.getHeaders().isEmpty()) { + List headerList = actionConfiguration.getHeaders().stream() + .filter(header -> !org.springframework.util.StringUtils.isEmpty(header.getKey())) + .collect(Collectors.toList()); + actionConfiguration.setHeaders(headerList); + } + + return this.execute(connection, datasourceConfiguration, actionConfiguration); + } + @Override public Mono execute(APIConnection apiConnection, DatasourceConfiguration datasourceConfiguration, @@ -579,5 +622,30 @@ public class RestApiPlugin extends BasePlugin { log.debug("Got request in actionExecutionResult as: {}", actionExecutionRequest); return actionExecutionRequest; } + + private ActionConfiguration updateActionConfigurationForPagination(ActionConfiguration actionConfiguration, + PaginationField paginationField) { + if (PaginationField.NEXT.equals(paginationField) || PaginationField.PREV.equals(paginationField)) { + actionConfiguration.setPath(""); + actionConfiguration.setQueryParameters(null); + } + return actionConfiguration; + } + + private DatasourceConfiguration updateDatasourceConfigurationForPagination(ActionConfiguration actionConfiguration, + DatasourceConfiguration datasourceConfiguration, + PaginationField paginationField) { + if (PaginationField.NEXT.equals(paginationField)) { + if (actionConfiguration.getNext() == null) { + datasourceConfiguration.setUrl(null); + } else { + datasourceConfiguration.setUrl(URLDecoder.decode(actionConfiguration.getNext(), StandardCharsets.UTF_8)); + } + } else if (PaginationField.PREV.equals(paginationField)) { + datasourceConfiguration.setUrl(actionConfiguration.getPrev()); + } + return datasourceConfiguration; + } } + } \ No newline at end of file diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java index 11062409f0..8433b403c3 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java @@ -5,7 +5,7 @@ import com.appsmith.server.constants.Url; import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.ActionMoveDTO; import com.appsmith.server.dtos.ActionViewDTO; -import com.appsmith.server.dtos.ExecuteActionDTO; +import com.appsmith.external.dtos.ExecuteActionDTO; import com.appsmith.server.dtos.LayoutDTO; import com.appsmith.server.dtos.RefactorNameDTO; import com.appsmith.server.dtos.ResponseDTO; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ExecuteActionDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ExecuteActionDTO.java deleted file mode 100644 index 1890470744..0000000000 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ExecuteActionDTO.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.appsmith.server.dtos; - -import com.appsmith.external.models.PaginationField; -import com.appsmith.external.models.Param; -import com.appsmith.server.domains.Action; -import lombok.Getter; -import lombok.Setter; - -import java.util.List; - -@Getter -@Setter -public class ExecuteActionDTO { - - /** - * action field was added to support dry run execution. Now that dry run functionality has been removed, - * actionId has been added to send only the id of the action. - * TODO : Remove the deprecated field. - */ - @Deprecated - Action action; - - String actionId; - - List params; - - PaginationField paginationField; - - Boolean viewMode = false; -} 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 e6b8885568..5d3889d3b3 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 @@ -26,6 +26,7 @@ import com.appsmith.server.domains.Plugin; import com.appsmith.server.domains.PluginType; import com.appsmith.server.domains.QApplication; import com.appsmith.server.domains.QDatasource; +import com.appsmith.server.domains.QNewAction; import com.appsmith.server.domains.QOrganization; import com.appsmith.server.domains.QPlugin; import com.appsmith.server.domains.Role; @@ -81,11 +82,11 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static com.appsmith.external.helpers.BeanCopyUtils.copyNewFieldValuesIntoOldObject; import static com.appsmith.server.acl.AclPermission.EXECUTE_ACTIONS; import static com.appsmith.server.acl.AclPermission.MAKE_PUBLIC_APPLICATIONS; import static com.appsmith.server.acl.AclPermission.ORGANIZATION_INVITE_USERS; import static com.appsmith.server.acl.AclPermission.READ_ACTIONS; -import static com.appsmith.server.helpers.BeanCopyUtils.copyNewFieldValuesIntoOldObject; import static com.appsmith.server.repositories.BaseAppsmithRepositoryImpl.fieldName; import static org.springframework.data.mongodb.core.query.Criteria.where; import static org.springframework.data.mongodb.core.query.Query.query; @@ -1622,7 +1623,7 @@ public class DatabaseChangelog { } @ChangeSet(order = "051", id = "add-amazons3-plugin", author = "") - public void addAmazonS3Plugin (MongoTemplate mongoTemplate) { + public void addAmazonS3Plugin(MongoTemplate mongoTemplate) { Plugin plugin = new Plugin(); plugin.setName("Amazon S3"); plugin.setType(PluginType.DB); @@ -1666,50 +1667,92 @@ public class DatabaseChangelog { } } - - @ChangeSet(order = "053", id = "update-plugin-datasource-form-components", author = "") - public void updatePluginDatasourceFormComponents(MongoTemplate mongoTemplate) { - for (Plugin plugin : mongoTemplate.findAll(Plugin.class)) { - switch (plugin.getPackageName()) { - case "postgres-plugin": - case "mongo-plugin": - case "elasticsearch-plugin": - case "dynamo-plugin": - case "redis-plugin": - case "mssql-plugin": - case "firestore-plugin": - case "redshift-plugin": - case "mysql-plugin": - case "amazons3-plugin": - plugin.setDatasourceComponent("AutoForm"); - break; - case "restapi-plugin": - plugin.setDatasourceComponent("RestAPIDatasourceForm"); - break; - default: - continue; - } - mongoTemplate.save(plugin); - } - } + @ChangeSet(order = "053", id = "update-plugin-datasource-form-components", author = "") + public void updatePluginDatasourceFormComponents(MongoTemplate mongoTemplate) { + for (Plugin plugin : mongoTemplate.findAll(Plugin.class)) { + switch (plugin.getPackageName()) { + case "postgres-plugin": + case "mongo-plugin": + case "elasticsearch-plugin": + case "dynamo-plugin": + case "redis-plugin": + case "mssql-plugin": + case "firestore-plugin": + case "redshift-plugin": + case "mysql-plugin": + case "amazons3-plugin": + plugin.setDatasourceComponent("AutoForm"); + break; + case "restapi-plugin": + plugin.setDatasourceComponent("RestAPIDatasourceForm"); + break; + default: + continue; + } + + mongoTemplate.save(plugin); + } + } @ChangeSet(order = "054", id = "update-database-encode-params-toggle", author = "") public void updateEncodeParamsToggle(MongoTemplate mongoTemplate) { + for (NewAction action : mongoTemplate.findAll(NewAction.class)) { - if(action.getPluginType() != null && action.getPluginType().equals("API")) { - if(action.getUnpublishedAction() != null - && action.getUnpublishedAction().getActionConfiguration() != null) { - action.getUnpublishedAction().getActionConfiguration().setEncodeParamsToggle(true); - } + if (action.getPluginType() != null && action.getPluginType().equals("API")) { - if(action.getPublishedAction() != null - && action.getPublishedAction().getActionConfiguration() != null) { - action.getPublishedAction().getActionConfiguration().setEncodeParamsToggle(true); - } - - mongoTemplate.save(action); } + if (action.getUnpublishedAction() != null + && action.getUnpublishedAction().getActionConfiguration() != null) { + action.getUnpublishedAction().getActionConfiguration().setEncodeParamsToggle(true); + } + + if (action.getPublishedAction() != null + && action.getPublishedAction().getActionConfiguration() != null) { + action.getPublishedAction().getActionConfiguration().setEncodeParamsToggle(true); + } + + mongoTemplate.save(action); + } + } + + @ChangeSet(order = "055", id = "update-postgres-plugin-preparedStatement-config", author = "") + public void updatePostgresActionsSetPreparedStatementConfiguration(MongoTemplate mongoTemplate) { + + List plugins = mongoTemplate.find( + query(new Criteria().andOperator( + where(fieldName(QPlugin.plugin.packageName)).is("postgres-plugin") + )), + Plugin.class); + + if (plugins.size() < 1) { + return; + } + + Plugin postgresPlugin = plugins.get(0); + + // Fetch all the actions built on top of a postgres database + List postgresActions = mongoTemplate.find( + query(new Criteria().andOperator( + where(fieldName(QNewAction.newAction.pluginId)).is(postgresPlugin.getId()) + )), + NewAction.class + ); + + for (NewAction action : postgresActions) { + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "false")); + + // We have found an action of postgres plugin type + if (action.getUnpublishedAction().getActionConfiguration() != null) { + action.getUnpublishedAction().getActionConfiguration().setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + } + + if (action.getPublishedAction() != null && action.getPublishedAction().getActionConfiguration() != null) { + action.getPublishedAction().getActionConfiguration().setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + } + + mongoTemplate.save(action); } } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceServiceImpl.java index c08c95ebec..1c9a21ae88 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceServiceImpl.java @@ -15,7 +15,7 @@ import com.appsmith.server.domains.Plugin; import com.appsmith.server.domains.User; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; -import com.appsmith.server.helpers.MustacheHelper; +import com.appsmith.external.helpers.MustacheHelper; import com.appsmith.server.helpers.PluginExecutorHelper; import com.appsmith.server.repositories.DatasourceRepository; import com.appsmith.server.repositories.NewActionRepository; @@ -42,7 +42,7 @@ import java.util.stream.Collectors; import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES; import static com.appsmith.server.acl.AclPermission.ORGANIZATION_MANAGE_APPLICATIONS; import static com.appsmith.server.acl.AclPermission.ORGANIZATION_READ_APPLICATIONS; -import static com.appsmith.server.helpers.BeanCopyUtils.copyNestedNonNullProperties; +import static com.appsmith.external.helpers.BeanCopyUtils.copyNestedNonNullProperties; @Slf4j @Service diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionServiceImpl.java index d9ad17dda7..3af6c3b750 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionServiceImpl.java @@ -13,7 +13,7 @@ import com.appsmith.server.dtos.RefactorNameDTO; import com.appsmith.server.dtos.LayoutDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; -import com.appsmith.server.helpers.MustacheHelper; +import com.appsmith.external.helpers.MustacheHelper; import com.appsmith.server.solutions.PageLoadActionsUtil; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -41,7 +41,7 @@ import java.util.regex.Pattern; import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES; -import static com.appsmith.server.helpers.MustacheHelper.extractWordsAndAddToSet; +import static com.appsmith.external.helpers.MustacheHelper.extractWordsAndAddToSet; import static java.util.stream.Collectors.toSet; @Service diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionService.java index d34efe9756..f637a884c5 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionService.java @@ -5,7 +5,7 @@ import com.appsmith.server.acl.AclPermission; import com.appsmith.server.domains.NewAction; import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.ActionViewDTO; -import com.appsmith.server.dtos.ExecuteActionDTO; +import com.appsmith.external.dtos.ExecuteActionDTO; import com.appsmith.server.dtos.LayoutActionUpdateDTO; import org.springframework.data.domain.Sort; import org.springframework.util.MultiValueMap; @@ -58,5 +58,9 @@ public interface NewActionService extends CrudService { Flux findByPageId(String pageId); + List extractMustacheKeysInOrder(String query); + + String replaceMustacheWithQuestionMark(String query, List mustacheBindings); + Mono updateActionsExecuteOnLoad(List actions, String pageId, List actionUpdates, List messages); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java index 905abed6a9..3adee5ffcc 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java @@ -1,13 +1,13 @@ package com.appsmith.server.services; +import com.appsmith.external.dtos.ExecuteActionDTO; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; +import com.appsmith.external.helpers.MustacheHelper; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.DatasourceConfiguration; -import com.appsmith.external.models.PaginationField; -import com.appsmith.external.models.PaginationType; import com.appsmith.external.models.Param; import com.appsmith.external.models.Policy; import com.appsmith.external.models.Property; @@ -29,11 +29,9 @@ import com.appsmith.server.domains.PluginType; import com.appsmith.server.domains.User; import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.ActionViewDTO; -import com.appsmith.server.dtos.ExecuteActionDTO; import com.appsmith.server.dtos.LayoutActionUpdateDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; -import com.appsmith.server.helpers.MustacheHelper; import com.appsmith.server.helpers.PluginExecutorHelper; import com.appsmith.server.helpers.PolicyUtils; import com.appsmith.server.repositories.NewActionRepository; @@ -54,8 +52,6 @@ import reactor.core.scheduler.Scheduler; import javax.lang.model.SourceVersion; import javax.validation.Validator; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -68,15 +64,16 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import java.util.stream.Collectors; +import static com.appsmith.external.helpers.BeanCopyUtils.copyNewFieldValuesIntoOldObject; import static com.appsmith.server.acl.AclPermission.EXECUTE_ACTIONS; import static com.appsmith.server.acl.AclPermission.EXECUTE_DATASOURCES; import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES; import static com.appsmith.server.acl.AclPermission.READ_ACTIONS; import static com.appsmith.server.acl.AclPermission.READ_PAGES; -import static com.appsmith.server.helpers.BeanCopyUtils.copyNewFieldValuesIntoOldObject; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; @@ -572,21 +569,19 @@ public class NewActionServiceImpl extends BaseService executionMono = Mono.just(datasource) .flatMap(datasourceContextService::getDatasourceContext) // Now that we have the context (connection details), execute the action. .flatMap( - resourceContext -> pluginExecutor.execute( + resourceContext -> pluginExecutor.executeParameterized( resourceContext.getConnection(), + executeActionDTO, datasourceConfiguration, actionConfiguration ) @@ -693,8 +688,8 @@ public class NewActionServiceImpl extends BaseService replaceParamsMap = executeActionDTO - .getParams() - .stream() - .collect(Collectors.toMap( - // Trimming here for good measure. If the keys have space on either side, - // Mustache won't be able to find the key. - // We also add a backslash before every double-quote or backslash character - // because we apply the template replacing in a JSON-stringified version of - // these properties, where these two characters are escaped. - p -> p.getKey().trim(), // .replaceAll("[\"\n\\\\]", "\\\\$0"), - Param::getValue, - // In case of a conflict, we pick the older value - (oldValue, newValue) -> oldValue) - ); - - datasourceConfigurationTemp = variableSubstitution(datasource.getDatasourceConfiguration(), replaceParamsMap); - actionConfigurationTemp = variableSubstitution(action.getActionConfiguration(), replaceParamsMap); - } else { - datasourceConfigurationTemp = datasource.getDatasourceConfiguration(); - actionConfigurationTemp = action.getActionConfiguration(); - } - - // If the action is paginated, update the configurations to update the correct URL. - if (action.getActionConfiguration() != null && - action.getActionConfiguration().getPaginationType() != null && - PaginationType.URL.equals(action.getActionConfiguration().getPaginationType()) && - executeActionDTO.getPaginationField() != null) { - datasourceConfiguration = updateDatasourceConfigurationForPagination(actionConfigurationTemp, datasourceConfigurationTemp, executeActionDTO.getPaginationField()); - actionConfiguration = updateActionConfigurationForPagination(actionConfigurationTemp, executeActionDTO.getPaginationField()); - } else { - datasourceConfiguration = datasourceConfigurationTemp; - actionConfiguration = actionConfigurationTemp; - } - - // Filter out any empty headers - if (actionConfiguration.getHeaders() != null && !actionConfiguration.getHeaders().isEmpty()) { - List headerList = actionConfiguration.getHeaders().stream() - .filter(header -> !StringUtils.isEmpty(header.getKey())) - .collect(Collectors.toList()); - actionConfiguration.setHeaders(headerList); - } - } - - private ActionConfiguration updateActionConfigurationForPagination(ActionConfiguration actionConfiguration, - PaginationField paginationField) { - if (PaginationField.NEXT.equals(paginationField) || PaginationField.PREV.equals(paginationField)) { - actionConfiguration.setPath(""); - actionConfiguration.setQueryParameters(null); - } - return actionConfiguration; - } - - private DatasourceConfiguration updateDatasourceConfigurationForPagination(ActionConfiguration actionConfiguration, - DatasourceConfiguration datasourceConfiguration, - PaginationField paginationField) { - if (PaginationField.NEXT.equals(paginationField)) { - if (actionConfiguration.getNext() == null) { - datasourceConfiguration.setUrl(null); - } else { - datasourceConfiguration.setUrl(URLDecoder.decode(actionConfiguration.getNext(), StandardCharsets.UTF_8)); - } - } else if (PaginationField.PREV.equals(paginationField)) { - datasourceConfiguration.setUrl(actionConfiguration.getPrev()); - } - return datasourceConfiguration; - } - /** * This function replaces the variables in the Object with the actual params */ @@ -1128,6 +1045,23 @@ public class NewActionServiceImpl extends BaseService extractMustacheKeysInOrder(String query) { + return MustacheHelper.extractMustacheKeysInOrder(query); + } + + @Override + public String replaceMustacheWithQuestionMark(String query, List mustacheBindings) { + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody(query); + Map replaceParamsMap = mustacheBindings + .stream() + .collect(Collectors.toMap(Function.identity(), v -> "?")); + + ActionConfiguration updatedActionConfiguration = MustacheHelper.renderFieldValues(actionConfiguration, replaceParamsMap); + return updatedActionConfiguration.getBody(); + } + private Mono updateDatasourcePolicyForPublicAction(Set actionPolicies, Datasource datasource) { if (datasource.getId() == null) { // This seems to be a nested datasource. Return as is. diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewPageServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewPageServiceImpl.java index 276b258e25..7049195ca8 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewPageServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewPageServiceImpl.java @@ -32,7 +32,7 @@ import java.util.Set; import java.util.stream.Collectors; import static com.appsmith.server.acl.AclPermission.READ_PAGES; -import static com.appsmith.server.helpers.BeanCopyUtils.copyNewFieldValuesIntoOldObject; +import static com.appsmith.external.helpers.BeanCopyUtils.copyNewFieldValuesIntoOldObject; @Service @Slf4j diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java index b5684ba48c..dcddaa75a4 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java @@ -18,7 +18,7 @@ import com.appsmith.server.dtos.InviteUsersDTO; import com.appsmith.server.dtos.ResetUserPasswordDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; -import com.appsmith.server.helpers.BeanCopyUtils; +import com.appsmith.external.helpers.BeanCopyUtils; import com.appsmith.server.helpers.PolicyUtils; import com.appsmith.server.notifications.EmailSender; import com.appsmith.server.repositories.ApplicationRepository; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/PageLoadActionsUtil.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/PageLoadActionsUtil.java index 433e97dfed..f579035dc5 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/PageLoadActionsUtil.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/PageLoadActionsUtil.java @@ -20,7 +20,7 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -import static com.appsmith.server.helpers.MustacheHelper.extractWordsAndAddToSet; +import static com.appsmith.external.helpers.MustacheHelper.extractWordsAndAddToSet; @Slf4j @Component diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionServiceTest.java index 41c43abf48..0f12447ca6 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionServiceTest.java @@ -23,8 +23,8 @@ import com.appsmith.server.domains.User; import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.ActionMoveDTO; import com.appsmith.server.dtos.ActionViewDTO; +import com.appsmith.external.dtos.ExecuteActionDTO; import com.appsmith.server.dtos.ApplicationAccessDTO; -import com.appsmith.server.dtos.ExecuteActionDTO; import com.appsmith.server.dtos.PageDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; @@ -514,7 +514,7 @@ public class ActionServiceTest { AppsmithPluginException pluginException = new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR); Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.execute(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(Mono.error(pluginException)); + Mockito.when(pluginExecutor.executeParameterized(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(Mono.error(pluginException)); Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); Mono executionResultMono = newActionService.executeAction(executeActionDTO); @@ -559,7 +559,7 @@ public class ActionServiceTest { AppsmithPluginException pluginException = new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR); Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.execute(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(Mono.error(pluginException)); + Mockito.when(pluginExecutor.executeParameterized(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(Mono.error(pluginException)); Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); Mono executionResultMono = newActionService.executeAction(executeActionDTO); @@ -599,7 +599,7 @@ public class ActionServiceTest { executeActionDTO.setViewMode(false); Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.execute(Mockito.any(), Mockito.any(), Mockito.any())) + Mockito.when(pluginExecutor.executeParameterized(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(Mono.error(new StaleConnectionException())).thenReturn(Mono.error(new StaleConnectionException())); Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); @@ -640,7 +640,7 @@ public class ActionServiceTest { executeActionDTO.setViewMode(false); Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.execute(Mockito.any(), Mockito.any(), Mockito.any())) + Mockito.when(pluginExecutor.executeParameterized(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) .thenAnswer(x -> Mono.delay(Duration.ofMillis(1000)).ofType(ActionExecutionResult.class)); Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); @@ -724,7 +724,7 @@ public class ActionServiceTest { mockResult.setBody("response-body"); Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.execute(Mockito.any(), Mockito.any(), Mockito.any())) + Mockito.when(pluginExecutor.executeParameterized(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) .thenThrow(new StaleConnectionException()) .thenReturn(Mono.just(mockResult)); Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); @@ -767,7 +767,7 @@ public class ActionServiceTest { private Mono executeAction(ExecuteActionDTO executeActionDTO, ActionConfiguration actionConfiguration, ActionExecutionResult mockResult) { Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.execute(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(Mono.just(mockResult)); + Mockito.when(pluginExecutor.executeParameterized(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(Mono.just(mockResult)); Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); Mono actionExecutionResultMono = newActionService.executeAction(executeActionDTO);