Merge branch 'release' of github.com:appsmithorg/appsmith into release
This commit is contained in:
commit
4e1a2839d9
|
|
@ -3,7 +3,7 @@
|
|||
"methods": "users",
|
||||
"headerKey": "Content-Type",
|
||||
"headerValue": "application/json",
|
||||
"headerValueBlank": "",
|
||||
"headerValueBlank": " ",
|
||||
"queryKey": "page",
|
||||
"queryValue": "2",
|
||||
"queryAndValue": "users?page=2",
|
||||
|
|
@ -34,5 +34,8 @@
|
|||
"deleteAction": "//div[contains(@id,'react-select') and contains(text(),'DELETE')]",
|
||||
"moustacheMethod": "{{Api.text}}",
|
||||
"nextUrl": ".data.next}}",
|
||||
"prevUrl": ".data.previous}}"
|
||||
"prevUrl": ".data.previous}}",
|
||||
"methodsWithParam": "users?page=2",
|
||||
"invalidHeader": "invalid",
|
||||
"invalidValue": "invalid"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,15 +106,10 @@ describe("API Panel Test Functionality", function() {
|
|||
});
|
||||
|
||||
it("Test GET Action for mock API with header and pagination", function() {
|
||||
const apiname = "FirstAPI";
|
||||
const apiname = "SecondAPI";
|
||||
cy.CreateAPI(apiname);
|
||||
cy.log("Creation of API Action successful");
|
||||
cy.EnterSourceDetailsWithHeader(
|
||||
testdata.baseUrl,
|
||||
testdata.methods,
|
||||
testdata.headerValueBlank,
|
||||
testdata.headerValueBlank,
|
||||
);
|
||||
cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods);
|
||||
cy.RunAPI();
|
||||
cy.ResponseStatusCheck(testdata.successStatusCode);
|
||||
cy.log("Response code check successful");
|
||||
|
|
@ -137,16 +132,9 @@ describe("API Panel Test Functionality", function() {
|
|||
});
|
||||
|
||||
it("API check with query params test API fetaure", function() {
|
||||
cy.CreateAPI("FirstAPI");
|
||||
cy.log("Creation of FirstAPI Action successful");
|
||||
cy.EnterSourceDetailsWithQueryParam(
|
||||
testdata.baseUrl,
|
||||
testdata.methods,
|
||||
testdata.headerValueBlank,
|
||||
testdata.headerValueBlank,
|
||||
testdata.queryKey,
|
||||
testdata.queryValue,
|
||||
);
|
||||
cy.CreateAPI("ThirdAPI");
|
||||
cy.log("Creation of API Action successful");
|
||||
cy.enterDatasourceAndPath(testdata.baseUrl, testdata.queryAndValue);
|
||||
cy.RunAPI();
|
||||
cy.ResponseStatusCheck("200 OK");
|
||||
cy.log("Response code check successful");
|
||||
|
|
@ -155,13 +143,13 @@ describe("API Panel Test Functionality", function() {
|
|||
});
|
||||
|
||||
it("API check with Invalid Header", function() {
|
||||
cy.CreateAPI("FirstAPI");
|
||||
cy.log("Creation of SecondAPI Action successful");
|
||||
cy.CreateAPI("FourthAPI");
|
||||
cy.log("Creation of API Action successful");
|
||||
cy.EnterSourceDetailsWithQueryParam(
|
||||
testdata.baseUrl,
|
||||
testdata.methods,
|
||||
testdata.headerKey,
|
||||
testdata.headerValueBlank,
|
||||
testdata.invalidValue,
|
||||
testdata.queryKey,
|
||||
testdata.queryValue,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,12 +8,7 @@ describe("API Panel Test Functionality", function() {
|
|||
cy.log("Navigation to API Panel screen successful");
|
||||
cy.CreateAPI("FirstAPI");
|
||||
cy.log("Creation of FirstAPI Action successful");
|
||||
cy.EnterSourceDetailsWithHeader(
|
||||
testdata.baseUrl,
|
||||
testdata.methods,
|
||||
testdata.headerKey,
|
||||
testdata.headerValue,
|
||||
);
|
||||
cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods);
|
||||
cy.RunAPI();
|
||||
cy.ResponseStatusCheck(testdata.successStatusCode);
|
||||
cy.get(apiwidget.createApiOnSideBar)
|
||||
|
|
|
|||
|
|
@ -121,5 +121,17 @@ describe("Create new org and share with a user", function() {
|
|||
expect($lis.eq(1)).to.contain(Cypress.env("TESTUSERNAME1"));
|
||||
expect($lis.eq(2)).to.contain(Cypress.env("TESTUSERNAME2"));
|
||||
});
|
||||
cy.xpath(homePage.appHome)
|
||||
.should("be.visible")
|
||||
.click();
|
||||
cy.wait("@applications").should(
|
||||
"have.nested.property",
|
||||
"response.body.responseMeta.status",
|
||||
200,
|
||||
);
|
||||
cy.SearchApp(appid);
|
||||
cy.get("#loading").should("not.exist");
|
||||
cy.wait("@getPropertyPane");
|
||||
cy.get("@getPropertyPane").should("have.property", "status", 200);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -362,18 +362,14 @@ Cypress.Commands.add("EditApiName", apiname => {
|
|||
|
||||
Cypress.Commands.add("WaitAutoSave", () => {
|
||||
// wait for save query to trigger
|
||||
cy.wait(200);
|
||||
cy.wait(2000);
|
||||
cy.wait("@saveAction");
|
||||
//cy.wait("@postExecute");
|
||||
});
|
||||
|
||||
Cypress.Commands.add("RunAPI", () => {
|
||||
cy.get(ApiEditor.ApiRunBtn).click({ force: true });
|
||||
cy.wait("@postExecute").should(
|
||||
"have.nested.property",
|
||||
"response.body.responseMeta.status",
|
||||
200,
|
||||
);
|
||||
cy.wait("@postExecute");
|
||||
});
|
||||
|
||||
Cypress.Commands.add("SaveAndRunAPI", () => {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@ Cypress.on("uncaught:exception", (err, runnable) => {
|
|||
return false;
|
||||
});
|
||||
|
||||
Cypress.on("fail", (error, runnable) => {
|
||||
debugger;
|
||||
throw error; // throw error to have test still fail
|
||||
return false;
|
||||
});
|
||||
|
||||
before(function() {
|
||||
cy.startServerAndRoutes();
|
||||
const username = Cypress.env("USERNAME");
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public class AppsmithPluginException extends Exception {
|
|||
}
|
||||
|
||||
public Integer getAppErrorCode() {
|
||||
return this.error.getAppErrorCode();
|
||||
return this.error == null ? 0 : this.error.getAppErrorCode();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
package com.appsmith.external.pluginExceptions;
|
||||
|
||||
public class StaleConnectionException extends RuntimeException {
|
||||
}
|
||||
|
|
@ -115,6 +115,22 @@
|
|||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-dependencies</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<includeScope>runtime</includeScope>
|
||||
<outputDirectory>${project.build.directory}/lib</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
package com.external.plugins;
|
||||
|
||||
import com.appsmith.external.models.*;
|
||||
import com.appsmith.external.models.ActionConfiguration;
|
||||
import com.appsmith.external.models.ActionExecutionResult;
|
||||
import com.appsmith.external.models.AuthenticationDTO;
|
||||
import com.appsmith.external.models.DatasourceConfiguration;
|
||||
import com.appsmith.external.models.DatasourceTestResult;
|
||||
import com.appsmith.external.models.Endpoint;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.plugins.BasePlugin;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
|
@ -13,9 +19,19 @@ import org.springframework.util.CollectionUtils;
|
|||
import org.springframework.util.StringUtils;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.sql.*;
|
||||
import java.sql.Connection;
|
||||
import java.util.*;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.ResultSetMetaData;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.appsmith.external.models.Connection.Mode.READ_ONLY;
|
||||
|
||||
|
|
@ -25,6 +41,7 @@ public class MySqlPlugin extends BasePlugin {
|
|||
|
||||
private static final String USER = "user";
|
||||
private static final String PASSWORD = "password";
|
||||
private static final int VALIDITY_CHECK_TIMEOUT = 5;
|
||||
|
||||
public MySqlPlugin(PluginWrapper wrapper) {
|
||||
super(wrapper);
|
||||
|
|
@ -39,11 +56,23 @@ public class MySqlPlugin extends BasePlugin {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Mono<Object> execute(Object connection, DatasourceConfiguration datasourceConfiguration,
|
||||
public Mono<Object> execute(Object connection,
|
||||
DatasourceConfiguration datasourceConfiguration,
|
||||
ActionConfiguration actionConfiguration) {
|
||||
|
||||
Connection conn = (Connection) connection;
|
||||
|
||||
try {
|
||||
if (conn == null || conn.isClosed() || !conn.isValid(VALIDITY_CHECK_TIMEOUT)) {
|
||||
log.info("Encountered stale connection in MySQL plugin. Reporting back.");
|
||||
throw new StaleConnectionException();
|
||||
}
|
||||
} catch (SQLException error) {
|
||||
// This exception is thrown only when the timeout to `isValid` is negative. Since, that's not the case,
|
||||
// here, this should never happen.
|
||||
log.error("Error checking validity of MySQL connection.", error);
|
||||
}
|
||||
|
||||
String query = actionConfiguration.getBody();
|
||||
|
||||
if (query == null) {
|
||||
|
|
@ -149,7 +178,7 @@ public class MySqlPlugin extends BasePlugin {
|
|||
configurationConnection != null && READ_ONLY.equals(configurationConnection.getMode()));
|
||||
return Mono.just(connection);
|
||||
} catch (SQLException e) {
|
||||
return pluginErrorMono("Error connecting to MySql.", e);
|
||||
return pluginErrorMono("Error connecting to MySQL: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -115,6 +115,22 @@
|
|||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-dependencies</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<includeScope>runtime</includeScope>
|
||||
<outputDirectory>${project.build.directory}/lib</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.appsmith.external.models.Endpoint;
|
|||
import com.appsmith.external.models.SSLDetails;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.plugins.BasePlugin;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import lombok.NonNull;
|
||||
|
|
@ -47,6 +48,7 @@ public class PostgresPlugin extends BasePlugin {
|
|||
private static final String PASSWORD = "password";
|
||||
private static final String SSL = "ssl";
|
||||
private static final String DATE_COLUMN_TYPE_NAME = "date";
|
||||
private static final int VALIDITY_CHECK_TIMEOUT = 5;
|
||||
|
||||
public PostgresPlugin(PluginWrapper wrapper) {
|
||||
super(wrapper);
|
||||
|
|
@ -61,12 +63,23 @@ public class PostgresPlugin extends BasePlugin {
|
|||
public static class PostgresPluginExecutor implements PluginExecutor {
|
||||
|
||||
@Override
|
||||
public Mono<Object> execute(@NonNull Object connection,
|
||||
public Mono<Object> execute(Object connection,
|
||||
DatasourceConfiguration datasourceConfiguration,
|
||||
ActionConfiguration actionConfiguration) {
|
||||
|
||||
Connection conn = (Connection) connection;
|
||||
|
||||
try {
|
||||
if (conn == null || conn.isClosed() || !conn.isValid(VALIDITY_CHECK_TIMEOUT)) {
|
||||
log.info("Encountered stale connection in Postgres plugin. Reporting back.");
|
||||
throw new StaleConnectionException();
|
||||
}
|
||||
} catch (SQLException error) {
|
||||
// This exception is thrown only when the timeout to `isValid` is negative. Since, that's not the case,
|
||||
// here, this should never happen.
|
||||
log.error("Error checking validity of Postgres connection.", error);
|
||||
}
|
||||
|
||||
String query = actionConfiguration.getBody();
|
||||
|
||||
if (query == null) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.appsmith.external.models.Property;
|
|||
import com.appsmith.external.models.Provider;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import com.appsmith.server.acl.AclPermission;
|
||||
import com.appsmith.server.acl.PolicyGenerator;
|
||||
|
|
@ -412,16 +413,34 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
|
|||
action.getPageId(), action.getId(), action.getName(), datasourceConfiguration,
|
||||
actionConfiguration);
|
||||
|
||||
return datasourceContextService
|
||||
.getDatasourceContext(datasource)
|
||||
//Now that we have the context (connection details, execute the action
|
||||
.flatMap(resourceContext -> pluginExecutor.execute(
|
||||
resourceContext.getConnection(),
|
||||
datasourceConfiguration,
|
||||
actionConfiguration))
|
||||
Mono<Object> executionMono = Mono.just(datasource)
|
||||
.flatMap(datasourceContextService::getDatasourceContext)
|
||||
// Now that we have the context (connection details), execute the action.
|
||||
.flatMap(
|
||||
resourceContext -> pluginExecutor.execute(
|
||||
resourceContext.getConnection(),
|
||||
datasourceConfiguration,
|
||||
actionConfiguration
|
||||
)
|
||||
);
|
||||
|
||||
return executionMono
|
||||
.onErrorResume(StaleConnectionException.class, error -> {
|
||||
log.info("Looks like the connection is stale. Retrying with a fresh context.");
|
||||
return datasourceContextService
|
||||
.deleteDatasourceContext(datasource.getId())
|
||||
.then(executionMono);
|
||||
})
|
||||
.timeout(Duration.ofMillis(timeoutDuration))
|
||||
.onErrorMap(
|
||||
StaleConnectionException.class,
|
||||
error -> new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
"Secondary stale connection error."
|
||||
)
|
||||
)
|
||||
.onErrorResume(e -> {
|
||||
log.debug("In the action execution error mode. Cause: {}", e.getMessage());
|
||||
log.debug("In the action execution error mode.", e);
|
||||
ActionExecutionResult result = new ActionExecutionResult();
|
||||
result.setBody(e.getMessage());
|
||||
result.setIsExecutionSuccess(false);
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ public class DatasourceContextServiceImpl implements DatasourceContextService {
|
|||
return Mono.just(datasourceContextMap.get(datasourceId));
|
||||
}
|
||||
|
||||
log.debug("Datasource context doesn't exist. Creating connection");
|
||||
log.debug("Datasource context doesn't exist. Creating connection.");
|
||||
|
||||
Mono<Datasource> datasourceMono;
|
||||
|
||||
|
|
@ -124,26 +124,24 @@ public class DatasourceContextServiceImpl implements DatasourceContextService {
|
|||
|
||||
@Override
|
||||
public Mono<DatasourceContext> deleteDatasourceContext(String datasourceId) {
|
||||
|
||||
DatasourceContext datasourceContext = datasourceContextMap.get(datasourceId);
|
||||
if (datasourceContext == null) {
|
||||
//No resource context exists for this resource. Return void;
|
||||
// No resource context exists for this resource. Return void.
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
Mono<Datasource> datasourceMono = datasourceService.findById(datasourceId, EXECUTE_DATASOURCES);
|
||||
|
||||
Mono<Plugin> pluginMono = datasourceMono
|
||||
.flatMap(datasource -> pluginService.findById(datasource.getPluginId()));
|
||||
|
||||
//Datasource Context has not been created for this resource on this machine. Create one now.
|
||||
Mono<PluginExecutor> pluginExecutorMono = pluginExecutorHelper.getPluginExecutor(pluginMono);
|
||||
|
||||
return Mono.zip(datasourceMono, pluginExecutorMono, ((datasource, pluginExecutor) -> {
|
||||
pluginExecutor.datasourceDestroy(datasourceContext.getConnection());
|
||||
datasourceContextMap.remove(datasourceId);
|
||||
return datasourceContext;
|
||||
}));
|
||||
return datasourceService
|
||||
.findById(datasourceId, EXECUTE_DATASOURCES)
|
||||
.zipWhen(datasource1 ->
|
||||
pluginExecutorHelper.getPluginExecutor(pluginService.findById(datasource1.getPluginId()))
|
||||
)
|
||||
.map(tuple -> {
|
||||
final Datasource datasource = tuple.getT1();
|
||||
final PluginExecutor pluginExecutor = tuple.getT2();
|
||||
log.info("Clearing datasource context for datasource ID {}.", datasource.getId());
|
||||
pluginExecutor.datasourceDestroy(datasourceContext.getConnection());
|
||||
return datasourceContextMap.remove(datasourceId);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.appsmith.external.models.Policy;
|
|||
import com.appsmith.external.models.Property;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import com.appsmith.server.acl.AclPermission;
|
||||
import com.appsmith.server.constants.FieldName;
|
||||
|
|
@ -517,6 +518,37 @@ public class ActionServiceTest {
|
|||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithUserDetails(value = "api_user")
|
||||
public void checkRecoveryFromStaleConnections() {
|
||||
ActionExecutionResult mockResult = new ActionExecutionResult();
|
||||
mockResult.setIsExecutionSuccess(true);
|
||||
mockResult.setBody("response-body");
|
||||
|
||||
Action action = new Action();
|
||||
ActionConfiguration actionConfiguration = new ActionConfiguration();
|
||||
actionConfiguration.setBody("select * from users");
|
||||
action.setActionConfiguration(actionConfiguration);
|
||||
|
||||
ExecuteActionDTO executeActionDTO = new ExecuteActionDTO();
|
||||
executeActionDTO.setAction(action);
|
||||
|
||||
Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor));
|
||||
Mockito.when(pluginExecutor.execute(Mockito.any(), Mockito.any(), Mockito.any()))
|
||||
.thenThrow(new StaleConnectionException())
|
||||
.thenReturn(Mono.just(mockResult));
|
||||
Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<ActionExecutionResult> actionExecutionResultMono = actionService.executeAction(executeActionDTO);
|
||||
|
||||
StepVerifier.create(actionExecutionResultMono)
|
||||
.assertNext(result -> {
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getBody()).isEqualTo(mockResult.getBody());
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
private void executeAndAssertAction(ExecuteActionDTO executeActionDTO, ActionConfiguration actionConfiguration, ActionExecutionResult mockResult) {
|
||||
|
||||
Mono<ActionExecutionResult> actionExecutionResultMono = executeAction(executeActionDTO, actionConfiguration, mockResult);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user