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:
parent
3ff938712c
commit
e5574c1945
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
15
app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/DataType.java
vendored
Normal file
15
app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/DataType.java
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package com.appsmith.external.constants;
|
||||
|
||||
public enum DataType {
|
||||
INTEGER,
|
||||
LONG,
|
||||
FLOAT,
|
||||
DOUBLE,
|
||||
BOOLEAN,
|
||||
DATE,
|
||||
TIME,
|
||||
ASCII,
|
||||
BINARY,
|
||||
BYTES,
|
||||
STRING
|
||||
}
|
||||
21
app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/ExecuteActionDTO.java
vendored
Normal file
21
app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/ExecuteActionDTO.java
vendored
Normal 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;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.appsmith.server.helpers;
|
||||
package com.appsmith.external.helpers;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeanWrapper;
|
||||
|
|
@ -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<>();
|
||||
|
||||
235
app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SqlStringUtils.java
vendored
Normal file
235
app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SqlStringUtils.java
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +47,6 @@ public class ActionConfiguration {
|
|||
// DB action fields
|
||||
|
||||
// JS action fields
|
||||
|
||||
String jsFunction;
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.appsmith.server.helpers;
|
||||
package com.appsmith.external.helpers;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
|
@ -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(
|
||||
|
|
@ -14,7 +14,8 @@
|
|||
{
|
||||
"label": "-- Select --",
|
||||
"value": ""
|
||||
},{
|
||||
},
|
||||
{
|
||||
"label": "List files in bucket",
|
||||
"value": "LIST"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user