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