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 <mohanarpit@users.noreply.github.com>

* Update app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SqlStringUtils.java

Co-authored-by: Arpit Mohan <mohanarpit@users.noreply.github.com>

* Incorporated review comments.

* Fixed compile time error.

Co-authored-by: Arpit Mohan <mohanarpit@users.noreply.github.com>
This commit is contained in:
Trisha Anand 2021-02-18 18:33:27 +05:30 committed by GitHub
parent 3ff938712c
commit e5574c1945
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 977 additions and 255 deletions

View File

@ -92,6 +92,29 @@
<artifactId>querydsl-jpa</artifactId>
<version>4.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.8</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>1.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View File

@ -0,0 +1,15 @@
package com.appsmith.external.constants;
public enum DataType {
INTEGER,
LONG,
FLOAT,
DOUBLE,
BOOLEAN,
DATE,
TIME,
ASCII,
BINARY,
BYTES,
STRING
}

View File

@ -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<Param> params;
PaginationField paginationField;
Boolean viewMode = false;
}

View File

@ -1,4 +1,4 @@
package com.appsmith.server.helpers;
package com.appsmith.external.helpers;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;

View File

@ -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<String> extractMustacheKeysInOrder(String template) {
List<String> 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<String> extractMustacheKeysFromFields(Object object) {
final Set<String> keys = new HashSet<>();

View File

@ -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<String> mustacheBindings) {
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setBody(query);
Map<String, String> 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;
}
}

View File

@ -47,7 +47,6 @@ public class ActionConfiguration {
// DB action fields
// JS action fields
String jsFunction;
/*

View File

@ -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<C> extends ExtensionPoint {
/**
* This function is used to execute the action.
* This function is implemented by the plugins by default to execute the action.
* <p>
* 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<C> 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.
*
* <p>
* 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<String> validateDatasource(DatasourceConfiguration datasourceConfiguration);
@ -85,4 +92,72 @@ public interface PluginExecutor<C> extends ExtensionPoint {
default Mono<DatasourceStructure> 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
* <p>
* 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<ActionExecutionResult> 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<String, String> 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);
}
}
}

View File

@ -1,4 +1,4 @@
package com.appsmith.server.helpers;
package com.appsmith.external.helpers;
import lombok.AllArgsConstructor;
import lombok.Getter;

View File

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

View File

@ -14,7 +14,8 @@
{
"label": "-- Select --",
"value": ""
},{
},
{
"label": "List files in bucket",
"value": "LIST"
},

View File

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

View File

@ -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<ActionExecutionResult> execute(HikariDataSource connection,
DatasourceConfiguration datasourceConfiguration,
ActionConfiguration actionConfiguration) {
public Mono<ActionExecutionResult> 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<Property> 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<String> 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<ActionExecutionResult> executeCommon(HikariDataSource connection,
DatasourceConfiguration datasourceConfiguration,
ActionConfiguration actionConfiguration,
Boolean preparedStatement,
List<String> 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<Param> params = executeActionDTO.getParams();
for (int i = 0; i < mustacheValuesInOrder.size(); i++) {
String key = mustacheValuesInOrder.get(i);
Optional<Param> 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<ActionExecutionResult> execute(HikariDataSource connection, DatasourceConfiguration datasourceConfiguration, ActionConfiguration actionConfiguration) {
// Unused function
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Unsupported Operation"));
}
@Override
public Mono<HikariDataSource> 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: <https://docs.oracle.com/en/java/javase/11/docs/api/java.sql/java/sql/DatabaseMetaData.html>.
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
*/

View File

@ -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",

View File

@ -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<Property> pluginSpecifiedTemplates = new ArrayList<>();
pluginSpecifiedTemplates.add(new Property("preparedStatement", "false"));
actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates);
Mono<ActionExecutionResult> 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<Property> pluginSpecifiedTemplates = new ArrayList<>();
pluginSpecifiedTemplates.add(new Property("preparedStatement", "false"));
actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates);
Mono<ActionExecutionResult> 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<Property> pluginSpecifiedTemplates = new ArrayList<>();
pluginSpecifiedTemplates.add(new Property("preparedStatement", "false"));
actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates);
Mono<HikariDataSource> connectionCreateMono = pluginExecutor.datasourceCreate(dsConfig);
Mono<ActionExecutionResult> 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<Property> pluginSpecifiedTemplates = new ArrayList<>();
pluginSpecifiedTemplates.add(new Property("preparedStatement", "true"));
actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates);
ExecuteActionDTO executeActionDTO = new ExecuteActionDTO();
List<Param> params = new ArrayList<>();
Param param = new Param();
param.setKey("binding1");
param.setValue("1");
params.add(param);
executeActionDTO.setParams(params);
Mono<HikariDataSource> connectionCreateMono = pluginExecutor.datasourceCreate(dsConfig).cache();
Mono<ActionExecutionResult> 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<Property> pluginSpecifiedTemplates = new ArrayList<>();
pluginSpecifiedTemplates.add(new Property("preparedStatement", "true"));
actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates);
ExecuteActionDTO executeActionDTO = new ExecuteActionDTO();
List<Param> params = new ArrayList<>();
Param param = new Param();
param.setKey("binding1");
param.setValue("1");
params.add(param);
executeActionDTO.setParams(params);
Mono<HikariDataSource> connectionCreateMono = pluginExecutor.datasourceCreate(dsConfig).cache();
Mono<ActionExecutionResult> 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<Property> pluginSpecifiedTemplates = new ArrayList<>();
pluginSpecifiedTemplates.add(new Property("preparedStatement", "true"));
actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates);
ExecuteActionDTO executeActionDTO = new ExecuteActionDTO();
List<Param> params = new ArrayList<>();
Param param = new Param();
param.setKey("binding1");
param.setValue("1");
params.add(param);
executeActionDTO.setParams(params);
Mono<HikariDataSource> connectionCreateMono = pluginExecutor.datasourceCreate(dsConfig).cache();
Mono<ActionExecutionResult> 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();
}
}

View File

@ -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<ActionExecutionResult> 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<Property> 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<ActionExecutionResult> 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;
}
}
}

View File

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

View File

@ -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<Param> params;
PaginationField paginationField;
Boolean viewMode = false;
}

View File

@ -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<Plugin> 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<NewAction> postgresActions = mongoTemplate.find(
query(new Criteria().andOperator(
where(fieldName(QNewAction.newAction.pluginId)).is(postgresPlugin.getId())
)),
NewAction.class
);
for (NewAction action : postgresActions) {
List<Property> 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);
}
}
}

View File

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

View File

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

View File

@ -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<NewAction, String> {
Flux<NewAction> findByPageId(String pageId);
List<String> extractMustacheKeysInOrder(String query);
String replaceMustacheWithQuestionMark(String query, List<String> mustacheBindings);
Mono<Boolean> updateActionsExecuteOnLoad(List<ActionDTO> actions, String pageId, List<LayoutActionUpdateDTO> actionUpdates, List<String> messages);
}

View File

@ -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<NewActionRepository, NewAc
DatasourceConfiguration datasourceConfiguration = datasource.getDatasourceConfiguration();
ActionConfiguration actionConfiguration = action.getActionConfiguration();
prepareConfigurationsForExecution(action, datasource, executeActionDTO, actionConfiguration, datasourceConfiguration);
Integer timeoutDuration = actionConfiguration.getTimeoutInMillisecond();
log.debug("[{}]Execute Action called in Page {}, for action id : {} action name : {}, {}, {}",
log.debug("[{}]Execute Action called in Page {}, for action id : {} action name : {}",
Thread.currentThread().getName(),
action.getPageId(), actionId, action.getName(), datasourceConfiguration,
actionConfiguration);
action.getPageId(), actionId, action.getName());
Mono<ActionExecutionResult> 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<NewActionRepository, NewAc
"properties", actionConfiguration.getPluginSpecifiedTemplates() == null
? Collections.emptyMap()
: actionConfiguration.getPluginSpecifiedTemplates()
.stream()
.collect(Collectors.toMap(Property::getKey, Property::getValue))
.stream()
.collect(Collectors.toMap(Property::getKey, Property::getValue))
));
}
@ -730,84 +725,6 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
.then();
}
private void prepareConfigurationsForExecution(ActionDTO action,
Datasource datasource,
ExecuteActionDTO executeActionDTO,
ActionConfiguration actionConfiguration,
DatasourceConfiguration datasourceConfiguration) {
DatasourceConfiguration datasourceConfigurationTemp;
ActionConfiguration actionConfigurationTemp;
//Do variable substitution
//Do this only if params have been provided in the execute command
if (executeActionDTO.getParams() != null && !executeActionDTO.getParams().isEmpty()) {
Map<String, String> 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<Property> 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<NewActionRepository, NewAc
.flatMap(analyticsService::sendDeleteEvent);
}
public List<String> extractMustacheKeysInOrder(String query) {
return MustacheHelper.extractMustacheKeysInOrder(query);
}
@Override
public String replaceMustacheWithQuestionMark(String query, List<String> mustacheBindings) {
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setBody(query);
Map<String, String> replaceParamsMap = mustacheBindings
.stream()
.collect(Collectors.toMap(Function.identity(), v -> "?"));
ActionConfiguration updatedActionConfiguration = MustacheHelper.renderFieldValues(actionConfiguration, replaceParamsMap);
return updatedActionConfiguration.getBody();
}
private Mono<Datasource> updateDatasourcePolicyForPublicAction(Set<Policy> actionPolicies, Datasource datasource) {
if (datasource.getId() == null) {
// This seems to be a nested datasource. Return as is.

View File

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

View File

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

View File

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

View File

@ -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<ActionExecutionResult> 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<ActionExecutionResult> 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<ActionExecutionResult> 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<ActionExecutionResult> actionExecutionResultMono = newActionService.executeAction(executeActionDTO);