feat: Introducing SaaS integrations as a plugin (#7560)

* WIP client side changes

* Saas execution flow + scheduled import of remote plugins
This commit is contained in:
Nidhi 2021-09-21 17:35:29 +05:30 committed by GitHub
parent b26fc9965a
commit c3f4cdaa15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 1123 additions and 78 deletions

View File

@ -391,7 +391,8 @@ function getIntegrationOptionsWithChildren(
const apis = actions.filter(
(action) =>
action.config.pluginType === PluginType.API ||
action.config.pluginType === PluginType.SAAS,
action.config.pluginType === PluginType.SAAS ||
action.config.pluginType === PluginType.REMOTE,
);
const option = options.find(
(option) => option.value === ActionType.integration,

View File

@ -104,6 +104,7 @@ function Command(props: {
DB: <DataSourcesColoredIcon />,
API: <ApisIcon />,
SAAS: <DataSourcesColoredIcon />,
REMOTE: <DataSourcesColoredIcon />,
JS: <JsIcon />,
}[props.pluginType]}
{props.imgSrc && <img src={props.imgSrc} />}

View File

@ -30,7 +30,11 @@ export const useActions = () => {
.map((action) => action.config);
const saas: Action[] = actions
.filter((action) => action.config.pluginType === PluginType.SAAS)
.filter(
(action) =>
action.config.pluginType === PluginType.SAAS ||
action.config.pluginType === PluginType.REMOTE,
)
.map((action) => action.config);
return { apis, queries, saas };

View File

@ -119,6 +119,7 @@ export const defaultActionSettings: Record<PluginType, any> = {
[PluginType.API]: apiActionSettingsConfig,
[PluginType.DB]: queryActionSettingsConfig,
[PluginType.SAAS]: saasActionSettingsConfig,
[PluginType.REMOTE]: saasActionSettingsConfig,
[PluginType.JS]: [],
};
@ -126,6 +127,7 @@ export const defaultActionEditorConfigs: Record<PluginType, any> = {
[PluginType.API]: apiActionEditorConfig,
[PluginType.DB]: [],
[PluginType.SAAS]: [],
[PluginType.REMOTE]: [],
[PluginType.JS]: [],
};
@ -136,5 +138,6 @@ export const defaultActionDependenciesConfig: Record<
[PluginType.API]: apiActionDependencyConfig,
[PluginType.DB]: {},
[PluginType.SAAS]: {},
[PluginType.REMOTE]: {},
[PluginType.JS]: {},
};

View File

@ -7,6 +7,7 @@ export enum PluginType {
DB = "DB",
SAAS = "SAAS",
JS = "JS",
REMOTE = "REMOTE",
}
export enum PaginationType {
@ -93,6 +94,11 @@ export interface SaaSAction extends BaseAction {
actionConfiguration: any;
datasource: StoredDatasource;
}
export interface RemoteAction extends BaseAction {
pluginType: PluginType.REMOTE;
actionConfiguration: any;
datasource: StoredDatasource;
}
export interface EmbeddedApiAction extends BaseApiAction {
datasource: EmbeddedRestDatasource;
@ -127,4 +133,4 @@ export type ActionViewMode = {
timeoutInMillisecond?: number;
};
export type Action = ApiAction | QueryAction | SaaSAction;
export type Action = ApiAction | QueryAction | SaaSAction | RemoteAction;

View File

@ -55,7 +55,7 @@ export type ActionGroupConfig = {
export const ACTION_PLUGIN_MAP: Array<ActionGroupConfig | undefined> = [
{
groupName: "Datasources",
types: [PluginType.API, PluginType.SAAS, PluginType.DB],
types: [PluginType.API, PluginType.SAAS, PluginType.DB, PluginType.REMOTE],
icon: dbQueryIcon,
key: generateReactKey(),
getURL: (
@ -72,7 +72,10 @@ export const ACTION_PLUGIN_MAP: Array<ActionGroupConfig | undefined> = [
plugin.packageName,
id,
)}`;
} else if (pluginType === PluginType.DB) {
} else if (
pluginType === PluginType.DB ||
pluginType === PluginType.REMOTE
) {
return `${QUERIES_EDITOR_ID_URL(applicationId, pageId, id)}`;
} else {
return `${API_EDITOR_ID_URL(applicationId, pageId, id)}`;
@ -164,6 +167,9 @@ export const getPluginGroups = (
...entries.filter(
(entry: any) => entry.config.pluginType === PluginType.DB,
),
...entries.filter(
(entry: any) => entry.config.pluginType === PluginType.REMOTE,
),
]
: entries;

View File

@ -294,7 +294,9 @@ function NewApiScreen(props: Props) {
</ApiCard>
)}
{plugins
.filter((p) => p.type === PluginType.SAAS)
.filter(
(p) => p.type === PluginType.SAAS || p.type === PluginType.REMOTE,
)
.map((p) => (
<ApiCard
className={`t--createBlankApi-${p.packageName}`}

View File

@ -20,9 +20,9 @@ import {
getPluginIdsOfPackageNames,
getPlugins,
getPluginImages,
getDBDatasources,
getAction,
getActionResponses,
getDBAndRemoteDatasources,
} from "selectors/entitiesSelector";
import { PLUGIN_PACKAGE_DBS } from "constants/QueryEditorConstants";
import { QueryAction, QueryActionConfig } from "entities/Action";
@ -248,7 +248,7 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => {
plugins: allPlugins,
runErrorMessage,
pluginIds: getPluginIdsOfPackageNames(state, PLUGIN_PACKAGE_DBS),
dataSources: getDBDatasources(state),
dataSources: getDBAndRemoteDatasources(state),
responses: getActionResponses(state),
isRunning: state.ui.queryPane.isRunning[props.match.params.queryId],
isDeleting: state.ui.queryPane.isDeleting[props.match.params.queryId],

View File

@ -160,7 +160,7 @@ function* handleQueryCreatedSaga(actionPayload: ReduxAction<QueryAction>) {
pluginId,
pluginType,
} = actionPayload.payload;
if (pluginType === PluginType.DB) {
if (pluginType === PluginType.DB || pluginType === PluginType.REMOTE) {
yield put(initialize(QUERY_EDITOR_FORM_NAME, actionPayload.payload));
const applicationId = yield select(getCurrentApplicationId);
const pageId = yield select(getCurrentPageId);
@ -182,8 +182,10 @@ function* handleQueryCreatedSaga(actionPayload: ReduxAction<QueryAction>) {
function* handleDatasourceCreatedSaga(actionPayload: ReduxAction<Datasource>) {
const plugin = yield select(getPlugin, actionPayload.payload.pluginId);
debugger;
// Only look at db plugins
if (plugin.type !== PluginType.DB) return;
if (plugin.type !== PluginType.DB && plugin.type !== PluginType.REMOTE)
return;
const applicationId = yield select(getCurrentApplicationId);
const pageId = yield select(getCurrentPageId);

View File

@ -181,6 +181,13 @@ export const getDBPlugins = createSelector(getPlugins, (plugins) =>
plugins.filter((plugin) => plugin.type === PluginType.DB),
);
export const getDBAndRemotePlugins = createSelector(getPlugins, (plugins) =>
plugins.filter(
(plugin) =>
plugin.type === PluginType.DB || plugin.type === PluginType.REMOTE,
),
);
export const getDatasourceByPluginId = (state: AppState, pluginId: string) =>
state.entities.datasources.list.filter((d) => d.pluginId === pluginId);
@ -197,6 +204,19 @@ export const getDBDatasources = createSelector(
},
);
export const getDBAndRemoteDatasources = createSelector(
getDBAndRemotePlugins,
getEntities,
(plugins, entities) => {
const datasources = entities.datasources.list;
const pluginIds = plugins.map((plugin) => plugin.id);
return datasources.filter((datasource) =>
pluginIds.includes(datasource.pluginId),
);
},
);
export const getQueryName = (state: AppState, actionId: string): string => {
const action = state.entities.actions.find((action: ActionData) => {
return action.config.id === actionId;

View File

@ -131,6 +131,10 @@
<artifactId>h2</artifactId>
<version>1.4.200</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
</dependency>
</dependencies>

View File

@ -0,0 +1,14 @@
package com.appsmith.external.dtos;
import com.appsmith.external.models.DatasourceConfiguration;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@AllArgsConstructor
@Getter
@Setter
public class DatasourceDTO {
String id;
DatasourceConfiguration datasourceConfiguration;
}

View File

@ -0,0 +1,18 @@
package com.appsmith.external.dtos;
import com.appsmith.external.models.ActionConfiguration;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ExecutePluginDTO {
String installationKey;
String pluginName;
String pluginVersion;
String actionTemplateName;
String datasourceTemplateName;
DatasourceDTO datasource;
ActionConfiguration actionConfiguration;
ExecuteActionDTO executeActionDTO;
}

View File

@ -5,6 +5,7 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.validator.constraints.Range;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.http.HttpMethod;
@ -75,6 +76,9 @@ public class ActionConfiguration implements AppsmithDomain {
*/
Map<String, Object> formData;
@Transient
String templateName;
public void setTimeoutInMillisecond(String timeoutInMillisecond) {
try {
this.timeoutInMillisecond = Integer.valueOf(timeoutInMillisecond);

View File

@ -7,7 +7,6 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Transient;
import reactor.core.publisher.Mono;
import java.util.Set;
@ -24,7 +23,6 @@ import java.util.Set;
@JsonSubTypes.Type(value = DBAuth.class, name = Authentication.DB_AUTH),
@JsonSubTypes.Type(value = OAuth2.class, name = Authentication.OAUTH2),
@JsonSubTypes.Type(value = BasicAuth.class, name = Authentication.BASIC),
@JsonSubTypes.Type(value = BasicAuth.class, name = Authentication.BASIC),
@JsonSubTypes.Type(value = ApiKeyAuth.class, name = Authentication.API_KEY),
@JsonSubTypes.Type(value = BearerTokenAuth.class, name = Authentication.BEARER_TOKEN)
})
@ -39,7 +37,6 @@ public class AuthenticationDTO implements AppsmithDomain {
SUCCESS
};
@Transient
String authenticationType;
AuthenticationStatus authenticationStatus;

View File

@ -1,8 +1,5 @@
package com.appsmith.server.domains;
package com.appsmith.external.models;
import com.appsmith.external.models.BaseDomain;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceStructure;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
@ -32,6 +29,8 @@ public class Datasource extends BaseDomain {
String organizationId;
String templateName;
DatasourceConfiguration datasourceConfiguration;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)

View File

@ -37,8 +37,10 @@ public class OAuth2 extends AuthenticationDTO {
Type grantType;
// Send tokens as query params if false
Boolean isTokenHeader = false;
// Send auth details in body if false
Boolean isAuthorizationHeader = false;
String clientId;

View File

@ -5,4 +5,6 @@ public interface SharedConfig {
int getCodecSize();
int getMaxResponseSize();
String getRemoteExecutionUrl();
}

View File

@ -30,5 +30,6 @@
<module>snowflakePlugin</module>
<module>arangoDBPlugin</module>
<module>jsPlugin</module>
<module>saasPlugin</module>
</modules>
</project>

View File

@ -65,6 +65,11 @@ public class PostgresPluginTest {
public int getMaxResponseSize() {
return 10000;
}
@Override
public String getRemoteExecutionUrl() {
return "";
}
}

View File

@ -57,6 +57,11 @@ public class RestApiPluginTest {
public int getMaxResponseSize() {
return 10000;
}
@Override
public String getRemoteExecutionUrl() {
return "";
}
}
RestApiPlugin.RestApiPluginExecutor pluginExecutor = new RestApiPlugin.RestApiPluginExecutor(new MockSharedConfig());

View File

@ -0,0 +1,5 @@
plugin.id=saas-plugin
plugin.class=com.external.plugins.SaasPlugin
plugin.version=1.0-SNAPSHOT
plugin.provider=tech@appsmith.com
plugin.dependencies=

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.external.plugins</groupId>
<artifactId>saasPlugin</artifactId>
<version>1.0-SNAPSHOT</version>
<name>saasPlugin</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<plugin.id>saas-plugin</plugin.id>
<plugin.class>com.external.plugins.SaasPlugin</plugin.class>
<plugin.version>1.0-SNAPSHOT</plugin.version>
<plugin.provider>tech@appsmith.com</plugin.provider>
<plugin.dependencies/>
</properties>
<dependencies>
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j-spring</artifactId>
<version>0.7.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.appsmith</groupId>
<artifactId>interfaces</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.3.RELEASE</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.3.RELEASE</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<version>5.2.3.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<configuration>
<minimizeJar>false</minimizeJar>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Plugin-Id>${plugin.id}</Plugin-Id>
<Plugin-Class>${plugin.class}</Plugin-Class>
<Plugin-Version>${plugin.version}</Plugin-Version>
<Plugin-Provider>${plugin.provider}</Plugin-Provider>
</manifestEntries>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,228 @@
package com.external.helpers;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserter;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* This receiver essentially instantiates a custom ClientHttpRequest that stores the request body via a subscriber
* The BodyInserter instance from our original request inserts into this subscriber that we can then retrieve
* using receiveValue.
*
* We do this so that the received value that we display to the user is exactly the same as
* the body tht is sent over the wire
*/
public class BodyReceiver {
private static final Object DUMMY = new Object();
private final AtomicReference<Object> reference = new AtomicReference<>(DUMMY);
public Object receiveValue(BodyInserter<?, ? extends ReactiveHttpOutputMessage> bodyInserter) {
demandValueFrom(bodyInserter);
return receivedValue();
}
private void demandValueFrom(BodyInserter<?, ? extends ReactiveHttpOutputMessage> bodyInserter) {
final BodyInserter<Object, MinimalHttpOutputMessage> inserter =
(BodyInserter<Object, MinimalHttpOutputMessage>) bodyInserter;
inserter.insert(
MinimalHttpOutputMessage.INSTANCE,
new SingleWriterContext(new WriteToConsumer<>(reference::set))
);
}
private Object receivedValue() {
Object value = reference.get();
reference.set(DUMMY);
Object validatedValue;
if (value == DUMMY) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR,
"Value was not received, check if your inserter worked properly");
} else {
validatedValue = value;
}
return validatedValue;
}
static class WriteToConsumer<T> implements HttpMessageWriter<T> {
private final Consumer<T> consumer;
private final List<MediaType> mediaTypes;
WriteToConsumer(Consumer<T> consumer) {
this.consumer = consumer;
this.mediaTypes = Collections.singletonList(MediaType.ALL);
}
@Override
public List<MediaType> getWritableMediaTypes() {
return mediaTypes;
}
@Override
public boolean canWrite(ResolvableType elementType, MediaType mediaType) {
return true;
}
@Override
public Mono<Void> write(
Publisher<? extends T> inputStream,
ResolvableType elementType,
MediaType mediaType,
ReactiveHttpOutputMessage message,
Map<String, Object> hints
) {
inputStream.subscribe(new OneValueConsumption<>(consumer));
return Mono.empty();
}
}
static class MinimalHttpOutputMessage implements ClientHttpRequest {
public static MinimalHttpOutputMessage INSTANCE = new MinimalHttpOutputMessage();
private MinimalHttpOutputMessage() {
}
@Override
public HttpHeaders getHeaders() {
return HttpHeaders.EMPTY;
}
@Override
public DataBufferFactory bufferFactory() {
return null;
}
@Override
public void beforeCommit(Supplier<? extends Mono<Void>> action) {
}
@Override
public boolean isCommitted() {
return false;
}
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
return null;
}
@Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
return null;
}
@Override
public Mono<Void> setComplete() {
return null;
}
@Override
public HttpMethod getMethod() {
return null;
}
@Override
public URI getURI() {
return null;
}
@Override
public MultiValueMap<String, HttpCookie> getCookies() {
return null;
}
}
static class OneValueConsumption<T> implements Subscriber<T> {
private final Consumer<T> consumer;
private int remainedAccepts;
public OneValueConsumption(Consumer<T> consumer) {
this.consumer = Objects.requireNonNull(consumer);
this.remainedAccepts = 1;
}
@Override
public void onSubscribe(Subscription s) {
s.request(1);
}
@Override
public void onNext(T o) {
if (remainedAccepts > 0) {
consumer.accept(o);
remainedAccepts -= 1;
} else {
throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "No more values can be consumed");
}
}
@Override
public void onError(Throwable t) {
throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Single value was not consumed", t);
}
@Override
public void onComplete() {
// nothing
}
}
static class SingleWriterContext implements BodyInserter.Context {
private final List<HttpMessageWriter<?>> singleWriterList;
SingleWriterContext(HttpMessageWriter<?> writer) {
this.singleWriterList = List.of(writer);
}
@Override
public List<HttpMessageWriter<?>> messageWriters() {
return singleWriterList;
}
@Override
public Optional<ServerHttpRequest> serverRequest() {
return Optional.empty();
}
@Override
public Map<String, Object> hints() {
return null;
}
}
}

View File

@ -0,0 +1,147 @@
package com.external.helpers;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionRequest;
import com.appsmith.external.models.Property;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
/**
* This filter captures the request that was sent out via WebClient so that the execution response can
* accurately represent the actual request used
*/
@Getter
public class RequestCaptureFilter implements ExchangeFilterFunction {
private ClientRequest request;
private final ObjectMapper objectMapper;
private final BodyReceiver bodyReceiver = new BodyReceiver();
public RequestCaptureFilter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
@NonNull
public Mono<ClientResponse> filter(@NonNull ClientRequest request, ExchangeFunction next) {
this.request = request;
return next.exchange(request);
}
public ActionExecutionRequest populateRequestFields(ActionExecutionRequest existing) {
final ActionExecutionRequest actionExecutionRequest = new ActionExecutionRequest();
actionExecutionRequest.setUrl(request.url().toString());
actionExecutionRequest.setHttpMethod(request.method());
MultiValueMap<String, String> headers = CollectionUtils.toMultiValueMap(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH));
AtomicBoolean isMultipart = new AtomicBoolean(false);
request.headers().forEach((header, value) -> {
if (HttpHeaders.AUTHORIZATION.equalsIgnoreCase(header) || "api_key".equalsIgnoreCase(header)) {
headers.add(header, "****");
} else {
headers.addAll(header, value);
}
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(header) && MULTIPART_FORM_DATA_VALUE.equalsIgnoreCase(value.get(0))) {
isMultipart.set(true);
}
});
actionExecutionRequest.setHeaders(objectMapper.valueToTree(headers));
actionExecutionRequest.setRequestParams(existing.getRequestParams());
actionExecutionRequest.setExecutionParameters(existing.getExecutionParameters());
actionExecutionRequest.setProperties(existing.getProperties());
// Apart from multipart, refer to the request that was actually sent
if (!isMultipart.get()) {
actionExecutionRequest.setBody(bodyReceiver.receiveValue(this.request.body()));
} else {
actionExecutionRequest.setBody(existing.getBody());
}
return actionExecutionRequest;
}
public static ActionExecutionRequest populateRequestFields(ActionConfiguration actionConfiguration,
URI uri,
List<Map.Entry<String, String>> insertedParams,
ObjectMapper objectMapper) {
ActionExecutionRequest actionExecutionRequest = new ActionExecutionRequest();
if (!insertedParams.isEmpty()) {
final Map<String, Object> requestData = new HashMap<>();
requestData.put("smart-substitution-parameters", insertedParams);
actionExecutionRequest.setProperties(requestData);
}
AtomicReference<String> reqContentType = new AtomicReference<>();
if (actionConfiguration.getHeaders() != null) {
MultiValueMap<String, String> reqMultiMap = CollectionUtils.toMultiValueMap(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH));
actionConfiguration.getHeaders()
.forEach(header -> {
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(header.getKey())) {
reqContentType.set((String) header.getValue());
}
reqMultiMap.put(header.getKey(), Arrays.asList((String) header.getValue()));
});
actionExecutionRequest.setHeaders(objectMapper.valueToTree(reqMultiMap));
}
// If the body is set, then use that field as the request body by default
if (actionConfiguration.getBody() != null) {
actionExecutionRequest.setBody(actionConfiguration.getBody());
}
if (MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(reqContentType.get())) {
final List<Property> bodyFormData = actionConfiguration.getBodyFormData();
Map<String, Object> bodyDataMap = new HashMap<>();
bodyFormData.forEach(property -> bodyDataMap.put(property.getKey(), property.getValue()));
actionExecutionRequest.setBody(bodyDataMap);
} else if (MediaType.MULTIPART_FORM_DATA_VALUE.equals(reqContentType.get())) {
final List<Property> bodyFormData = actionConfiguration.getBodyFormData();
Map<String, Object> bodyDataMap = new HashMap<>();
bodyFormData.forEach(property -> {
if ("FILE".equalsIgnoreCase(property.getType())) {
bodyDataMap.put(property.getKey(), "<file>");
} else {
bodyDataMap.put(property.getKey(), property.getValue());
}
});
actionExecutionRequest.setBody(bodyDataMap);
}
if (actionConfiguration.getHttpMethod() != null) {
actionExecutionRequest.setHttpMethod(actionConfiguration.getHttpMethod());
}
if (uri != null) {
actionExecutionRequest.setUrl(uri.toString());
}
return actionExecutionRequest;
}
}

View File

@ -0,0 +1,219 @@
package com.external.plugins;
import com.appsmith.external.dtos.ExecutePluginDTO;
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.plugins.BasePlugin;
import com.appsmith.external.plugins.PluginExecutor;
import com.appsmith.external.plugins.SmartSubstitutionInterface;
import com.appsmith.external.services.SharedConfig;
import com.external.helpers.RequestCaptureFilter;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.pf4j.Extension;
import org.pf4j.PluginWrapper;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.Exceptions;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Set;
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
public class SaasPlugin extends BasePlugin {
private static final int MAX_REDIRECTS = 5;
public SaasPlugin(PluginWrapper wrapper) {
super(wrapper);
}
@Extension
public static class SaasPluginExecutor implements PluginExecutor<ExecutePluginDTO>, SmartSubstitutionInterface {
private final SharedConfig sharedConfig;
// Setting max content length. This would've been coming from `spring.codec.max-in-memory-size` property if the
// `WebClient` instance was loaded as an auto-wired bean.
private final ExchangeStrategies EXCHANGE_STRATEGIES;
private final ObjectMapper saasObjectMapper = new ObjectMapper();
public SaasPluginExecutor(SharedConfig sharedConfig) {
this.sharedConfig = sharedConfig;
saasObjectMapper.disable(MapperFeature.USE_ANNOTATIONS);
saasObjectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
saasObjectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
this.EXCHANGE_STRATEGIES = ExchangeStrategies
.builder()
.codecs(clientDefaultCodecsConfigurer -> {
clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(saasObjectMapper, MediaType.APPLICATION_JSON));
clientDefaultCodecsConfigurer.defaultCodecs().maxInMemorySize(sharedConfig.getCodecSize());
})
.build();
}
@Override
public Mono<ActionExecutionResult> execute(ExecutePluginDTO connection, DatasourceConfiguration datasourceConfiguration, ActionConfiguration actionConfiguration) {
// Initializing object for error condition
ActionExecutionResult errorResult = new ActionExecutionResult();
final String datasourceConfigurationCommand = datasourceConfiguration.getAuthentication().getAuthenticationType();
if (datasourceConfigurationCommand == null || datasourceConfigurationCommand.isEmpty()) {
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, "Missing template name for datasource"));
}
final String actionConfigurationCommand = (String) actionConfiguration.getFormData().get("command");
if (actionConfigurationCommand == null || actionConfigurationCommand.isEmpty()) {
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "Missing template name for action"));
}
connection.setActionConfiguration(actionConfiguration);
connection.setDatasourceTemplateName(datasourceConfigurationCommand);
connection.setActionTemplateName(actionConfigurationCommand);
UriComponentsBuilder uriBuilder = UriComponentsBuilder.newInstance();
URI uri = null;
try {
uri = uriBuilder.uri(new URI(sharedConfig.getRemoteExecutionUrl())).build(true).toUri();
} catch (URISyntaxException e) {
e.printStackTrace();
}
ActionExecutionRequest actionExecutionRequest =
RequestCaptureFilter.populateRequestFields(actionConfiguration, uri, List.of(), objectMapper);
// Initializing webClient to be used for http call
WebClient.Builder webClientBuilder = WebClient.builder();
webClientBuilder.defaultHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE);
final RequestCaptureFilter requestCaptureFilter = new RequestCaptureFilter(objectMapper);
webClientBuilder.filter(requestCaptureFilter);
WebClient client = webClientBuilder.exchangeStrategies(EXCHANGE_STRATEGIES).build();
String valueAsString = "";
try {
valueAsString = saasObjectMapper.writeValueAsString(connection);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
Object requestBodyObj = BodyInserters.fromValue(valueAsString);
// Triggering the actual REST API call
return httpCall(client, HttpMethod.POST, uri, requestBodyObj, 0, APPLICATION_JSON_VALUE)
.flatMap(clientResponse -> clientResponse.toEntity(byte[].class))
.map(stringResponseEntity -> {
final HttpStatus statusCode = stringResponseEntity.getStatusCode();
byte[] body = stringResponseEntity.getBody();
if (statusCode.is2xxSuccessful()) {
try {
return saasObjectMapper.readValue(body, ActionExecutionResult.class);
} catch (IOException e) {
throw Exceptions.propagate(
new AppsmithPluginException(
AppsmithPluginError.PLUGIN_JSON_PARSE_ERROR,
body,
e.getMessage()
)
);
}
} else {
throw Exceptions.propagate(
new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR,
body
)
);
}
})
.onErrorResume(error -> {
errorResult.setRequest(requestCaptureFilter.populateRequestFields(actionExecutionRequest));
errorResult.setIsExecutionSuccess(false);
errorResult.setErrorInfo(error);
return Mono.just(errorResult);
});
}
private Mono<ClientResponse> httpCall(WebClient webClient, HttpMethod httpMethod, URI uri, Object requestBody,
int iteration, String contentType) {
if (iteration == MAX_REDIRECTS) {
return Mono.error(new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR,
"Exceeded the HTTP redirect limits of " + MAX_REDIRECTS
));
}
assert requestBody instanceof BodyInserter<?, ?>;
BodyInserter<?, ?> finalRequestBody = (BodyInserter<?, ?>) requestBody;
return webClient
.method(httpMethod)
.uri(uri)
.body((BodyInserter<?, ? super ClientHttpRequest>) finalRequestBody)
.exchange()
.doOnError(e -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e)))
.flatMap(response -> {
if (response.statusCode().is3xxRedirection()) {
String redirectUrl = response.headers().header("Location").get(0);
/**
* TODO
* In case the redirected URL is not absolute (complete), create the new URL using the relative path
* This particular scenario is seen in the URL : https://rickandmortyapi.com/api/character
* It redirects to partial URI : /api/character/
* In this scenario we should convert the partial URI to complete URI
*/
URI redirectUri;
try {
redirectUri = new URI(redirectUrl);
} catch (URISyntaxException e) {
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e));
}
return httpCall(webClient, httpMethod, redirectUri, finalRequestBody, iteration + 1,
contentType);
}
return Mono.just(response);
});
}
@Override
public Mono<ExecutePluginDTO> datasourceCreate(DatasourceConfiguration datasourceConfiguration) {
return Mono.empty();
}
@Override
public void datasourceDestroy(ExecutePluginDTO connection) {
}
@Override
public Set<String> validateDatasource(DatasourceConfiguration datasourceConfiguration) {
return Set.of();
}
@Override
public Mono<DatasourceTestResult> testDatasource(DatasourceConfiguration datasourceConfiguration) {
return Mono.empty();
}
}
}

View File

@ -5,7 +5,7 @@ import com.appsmith.server.domains.Action;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Comment;
import com.appsmith.server.domains.CommentThread;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.Page;
import com.appsmith.server.domains.User;

View File

@ -15,6 +15,9 @@ public class SharedConfigImpl implements SharedConfig {
@Value("${appsmith.plugin.response.size.max:5}")
private float maxPluginResponseSize = 5;
@Value("${appsmith.cloud_services.base_url}")
private String cloudServicesBaseUrl;
@Override
public int getCodecSize() {
return this.CODEC_SIZE * 1024 * 1024;
@ -24,4 +27,9 @@ public class SharedConfigImpl implements SharedConfig {
public int getMaxResponseSize() {
return (int) (this.maxPluginResponseSize * 1024 * 1024);
}
@Override
public String getRemoteExecutionUrl() {
return cloudServicesBaseUrl + "/api/v1/actions/execute";
}
}

View File

@ -5,7 +5,7 @@ import com.appsmith.external.models.DatasourceStructure;
import com.appsmith.external.models.DatasourceTestResult;
import com.appsmith.external.models.Property;
import com.appsmith.server.constants.Url;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.dtos.AuthorizationCodeCallbackDTO;
import com.appsmith.server.dtos.MockDataSet;
import com.appsmith.server.dtos.MockDataSource;

View File

@ -1,7 +1,7 @@
package com.appsmith.server.controllers;
import com.appsmith.server.constants.Url;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.dtos.ResponseDTO;
import com.appsmith.server.solutions.AuthenticationService;
import lombok.extern.slf4j.Slf4j;

View File

@ -2,6 +2,7 @@ package com.appsmith.server.domains;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.BaseDomain;
import com.appsmith.external.models.Datasource;
import com.appsmith.external.models.Property;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;

View File

@ -1,5 +1,6 @@
package com.appsmith.server.domains;
import com.appsmith.external.models.Datasource;
import com.appsmith.external.models.DecryptedSensitiveFields;
import lombok.Getter;
import lombok.Setter;

View File

@ -30,6 +30,8 @@ public class Plugin extends BaseDomain {
String packageName;
String pluginName;
String jarLocation;
String iconLocation;
@ -69,6 +71,14 @@ public class Plugin extends BaseDomain {
Boolean allowUserDatasources = true;
boolean isRemotePlugin = false;
// Stores the equivalent of editor.json for remote plugins
Map actionUiConfig;
// Stores the equivalent of form.json for remote plugins
Map datasourceUiConfig;
@Transient
Map<String, String> templates;

View File

@ -1,5 +1,5 @@
package com.appsmith.server.domains;
public enum PluginType {
DB, API, JS, SAAS
DB, API, JS, SAAS, REMOTE
}

View File

@ -4,7 +4,7 @@ import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.Policy;
import com.appsmith.external.models.Property;
import com.appsmith.server.domains.ActionProvider;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Documentation;
import com.appsmith.server.domains.PluginType;
import com.fasterxml.jackson.annotation.JsonIgnore;

View File

@ -7,7 +7,7 @@ import com.appsmith.server.acl.PolicyGenerator;
import com.appsmith.server.domains.ActionCollection;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.CommentThread;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.User;

View File

@ -5,9 +5,11 @@ import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.BaseDomain;
import com.appsmith.external.models.Connection;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.Datasource;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.Policy;
import com.appsmith.external.models.Property;
import com.appsmith.external.models.QDatasource;
import com.appsmith.external.models.SSLDetails;
import com.appsmith.external.services.EncryptionService;
import com.appsmith.server.acl.AclPermission;
@ -18,7 +20,6 @@ import com.appsmith.server.domains.Action;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Collection;
import com.appsmith.server.domains.Config;
import com.appsmith.server.domains.Datasource;
import com.appsmith.server.domains.Group;
import com.appsmith.server.domains.InviteUser;
import com.appsmith.server.domains.Layout;
@ -33,7 +34,6 @@ import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.PluginType;
import com.appsmith.server.domains.QApplication;
import com.appsmith.server.domains.QConfig;
import com.appsmith.server.domains.QDatasource;
import com.appsmith.server.domains.QNewAction;
import com.appsmith.server.domains.QOrganization;
import com.appsmith.server.domains.QPlugin;

View File

@ -2,7 +2,7 @@ package com.appsmith.server.repositories;
import com.appsmith.external.models.DatasourceStructure;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.mongodb.client.result.UpdateResult;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

View File

@ -1,9 +1,9 @@
package com.appsmith.server.repositories;
import com.appsmith.external.models.Datasource;
import com.appsmith.external.models.DatasourceStructure;
import com.appsmith.external.models.QDatasource;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.domains.Datasource;
import com.appsmith.server.domains.QDatasource;
import com.mongodb.client.result.UpdateResult;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.convert.MongoConverter;

View File

@ -1,6 +1,6 @@
package com.appsmith.server.repositories;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;

View File

@ -2,7 +2,7 @@ package com.appsmith.server.services;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.OAuth2;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.solutions.AuthenticationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@ -2,7 +2,7 @@ package com.appsmith.server.services;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Config;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

View File

@ -3,7 +3,7 @@ package com.appsmith.server.services;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Config;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.repositories.ApplicationRepository;

View File

@ -4,7 +4,7 @@ import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.Property;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.dtos.ActionDTO;
import com.appsmith.server.exceptions.AppsmithError;

View File

@ -1,6 +1,6 @@
package com.appsmith.server.services;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.DatasourceContext;
import reactor.core.publisher.Mono;

View File

@ -1,11 +1,10 @@
package com.appsmith.server.services;
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.UpdatableConnection;
import com.appsmith.external.plugins.PluginExecutor;
import com.appsmith.external.services.EncryptionService;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.DatasourceContext;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.helpers.PluginExecutorHelper;
@ -58,7 +57,7 @@ public class DatasourceContextServiceImpl implements DatasourceContextService {
log.debug("This is a dry run or an embedded datasource. The datasource context would not exist in this scenario");
} else if (datasourceContextMap.get(datasourceId) != null
// The following condition happens when there's a timout in the middle of destroying a connection and
// The following condition happens when there's a timeout in the middle of destroying a connection and
// the reactive flow interrupts, resulting in the destroy operation not completing.
&& datasourceContextMap.get(datasourceId).getConnection() != null
&& !isStale) {

View File

@ -2,7 +2,7 @@ package com.appsmith.server.services;
import com.appsmith.external.models.DatasourceTestResult;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

View File

@ -10,7 +10,7 @@ import com.appsmith.external.plugins.PluginExecutor;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.acl.PolicyGenerator;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.User;

View File

@ -2,7 +2,7 @@ package com.appsmith.server.services;
import com.appsmith.external.models.ApiTemplate;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Documentation;
import com.appsmith.server.dtos.ActionDTO;
import com.appsmith.server.dtos.AddItemToPageDTO;

View File

@ -8,7 +8,7 @@ import com.appsmith.external.models.DynamicBinding;
import com.appsmith.server.constants.AnalyticsEvents;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.ActionDependencyEdge;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.NewPage;

View File

@ -1,6 +1,6 @@
package com.appsmith.server.services;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.dtos.MockDataDTO;
import com.appsmith.server.dtos.MockDataSource;
import reactor.core.publisher.Mono;

View File

@ -8,7 +8,7 @@ import com.appsmith.external.models.Property;
import com.appsmith.external.models.SSLDetails;
import com.appsmith.server.configurations.CloudServicesConfig;
import com.appsmith.server.constants.AnalyticsEvents;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.User;
import com.appsmith.server.dtos.MockDataCredentials;
import com.appsmith.server.dtos.MockDataDTO;
@ -28,10 +28,8 @@ import reactor.core.publisher.Mono;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.apache.commons.lang.ObjectUtils.defaultIfNull;

View File

@ -1,6 +1,8 @@
package com.appsmith.server.services;
import com.appsmith.external.dtos.DatasourceDTO;
import com.appsmith.external.dtos.ExecuteActionDTO;
import com.appsmith.external.dtos.ExecutePluginDTO;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
@ -8,6 +10,7 @@ import com.appsmith.external.helpers.MustacheHelper;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionRequest;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.Datasource;
import com.appsmith.external.models.Param;
import com.appsmith.external.models.Policy;
import com.appsmith.external.models.Provider;
@ -20,7 +23,7 @@ import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Action;
import com.appsmith.server.domains.ActionProvider;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Datasource;
import com.appsmith.server.domains.DatasourceContext;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.Page;
@ -98,6 +101,7 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
private final PolicyUtils policyUtils;
private final ObjectMapper objectMapper;
private final AuthenticationValidator authenticationValidator;
private final ConfigService configService;
public NewActionServiceImpl(Scheduler scheduler,
Validator validator,
@ -115,7 +119,8 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
ApplicationService applicationService,
SessionUserService sessionUserService,
PolicyUtils policyUtils,
AuthenticationValidator authenticationValidator) {
AuthenticationValidator authenticationValidator,
ConfigService configService) {
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService);
this.repository = repository;
this.datasourceService = datasourceService;
@ -129,6 +134,7 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
this.sessionUserService = sessionUserService;
this.policyUtils = policyUtils;
this.authenticationValidator = authenticationValidator;
this.configService = configService;
this.objectMapper = new ObjectMapper();
}
@ -534,7 +540,8 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
}
return pluginService.findById(datasource.getPluginId());
})
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.PLUGIN)));
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.PLUGIN)))
.cache();
Mono<PluginExecutor> pluginExecutorMono = pluginExecutorHelper.getPluginExecutor(pluginMono);
@ -543,12 +550,14 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
.zip(
actionDTOMono,
datasourceMono,
pluginExecutorMono
pluginExecutorMono,
pluginMono
)
.flatMap(tuple -> {
final ActionDTO action = tuple.getT1();
final Datasource datasource = tuple.getT2();
final PluginExecutor pluginExecutor = tuple.getT3();
final Plugin plugin = tuple.getT4();
// Set the action name
actionName.set(action.getName());
@ -564,7 +573,13 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
Mono<Datasource> validatedDatasourceMono = authenticationValidator.validateAuthentication(datasource).cache();
Mono<ActionExecutionResult> executionMono = validatedDatasourceMono
.flatMap(datasourceContextService::getDatasourceContext)
.flatMap(datasource1 -> {
if (plugin.isRemotePlugin()) {
return this.getRemoteDatasourceContext(plugin, datasource1);
} else {
return datasourceContextService.getDatasourceContext(datasource1);
}
})
// Now that we have the context (connection details), execute the action.
.flatMap(resourceContext -> validatedDatasourceMono
.flatMap(datasource1 -> {
@ -1035,6 +1050,23 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
.filter(actionDTO -> !PluginType.JS.equals(actionDTO.getPluginType()));
}
// We can afford to make this call all the time since we already have all the info we need in context
private Mono<DatasourceContext> getRemoteDatasourceContext(Plugin plugin, Datasource datasource) {
final DatasourceContext datasourceContext = new DatasourceContext();
return configService.getInstanceId()
.map(instanceId -> {
ExecutePluginDTO executePluginDTO = new ExecutePluginDTO();
executePluginDTO.setInstallationKey(instanceId);
executePluginDTO.setPluginName(plugin.getPluginName());
executePluginDTO.setPluginVersion(plugin.getVersion());
executePluginDTO.setDatasource(new DatasourceDTO(datasource.getId(), datasource.getDatasourceConfiguration()));
datasourceContext.setConnection(executePluginDTO);
return datasourceContext;
});
}
@Override
public Mono<NewAction> save(NewAction action) {
// gitSyncId will be used to sync resource across instances

View File

@ -39,4 +39,6 @@ public interface OrganizationService extends CrudService<Organization, String> {
Mono<Organization> uploadLogo(String organizationId, Part filePart);
Mono<Organization> deleteLogo(String organizationId);
Flux<Organization> getAll();
}

View File

@ -338,4 +338,9 @@ public class OrganizationServiceImpl extends BaseService<OrganizationRepository,
});
}
@Override
public Flux<Organization> getAll() {
return repository.findAll();
}
}

View File

@ -1,6 +1,6 @@
package com.appsmith.server.services;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.dtos.InstallPluginRedisDTO;
@ -8,6 +8,7 @@ import com.appsmith.server.dtos.PluginOrgDTO;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
public interface PluginService extends CrudService<Plugin, String> {
@ -16,6 +17,8 @@ public interface PluginService extends CrudService<Plugin, String> {
Mono<Organization> installPlugin(PluginOrgDTO plugin);
Flux<Organization> installDefaultPlugins(List<Plugin> plugins);
Mono<Organization> uninstallPlugin(PluginOrgDTO plugin);
Mono<Plugin> findByName(String name);
@ -30,9 +33,13 @@ public interface PluginService extends CrudService<Plugin, String> {
Mono<Map> getFormConfig(String pluginId);
Flux<Plugin> getAllRemotePlugins();
Mono<Map> loadPluginResource(String pluginId, String resourcePath);
Mono<Map> getEditorConfigLabelMap(String pluginId);
Map loadEditorPluginResourceUqi(Plugin plugin);
Flux<Plugin> saveAll(Iterable<Plugin> plugins);
}

View File

@ -1,7 +1,7 @@
package com.appsmith.server.services;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Datasource;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.OrganizationPlugin;
import com.appsmith.server.domains.Plugin;
@ -185,6 +185,22 @@ public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, Str
.switchIfEmpty(Mono.empty());
}
@Override
public Flux<Organization> installDefaultPlugins(List<Plugin> plugins) {
final List<OrganizationPlugin> newOrganizationPlugins = plugins
.stream()
.filter(plugin -> Boolean.TRUE.equals(plugin.getDefaultInstall()))
.map(plugin -> {
return new OrganizationPlugin(plugin.getId(), OrganizationPluginStatus.ACTIVATED);
})
.collect(Collectors.toList());
return organizationService.getAll()
.flatMap(organization -> {
organization.getPlugins().addAll(newOrganizationPlugins);
return organizationService.save(organization);
});
}
@Override
public Mono<Organization> uninstallPlugin(PluginOrgDTO pluginDTO) {
if (pluginDTO.getPluginId() == null) {
@ -626,6 +642,16 @@ public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, Str
});
}
@Override
public Flux<Plugin> saveAll(Iterable<Plugin> plugins) {
return repository.saveAll(plugins);
}
@Override
public Flux<Plugin> getAllRemotePlugins() {
return repository.findByType(PluginType.REMOTE);
}
private Map loadPluginResourceGivenPlugin(Plugin plugin, String resourcePath) {
InputStream resourceAsStream = pluginManager
.getPlugin(plugin.getPackageName())
@ -649,8 +675,21 @@ public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, Str
public Mono<Map> loadPluginResource(String pluginId, String resourcePath) {
return findById(pluginId)
.map(plugin -> {
if (resourcePath.equals("editor.json") && plugin.getUiComponent().equals(UQI_DB_EDITOR_FORM)) {
return loadEditorPluginResourceUqi(plugin);
if ("editor.json".equals(resourcePath)) {
// UI config will be available if this plugin is sourced from the cloud
if (plugin.getActionUiConfig() != null) {
return plugin.getActionUiConfig();
}
// For UQI, use another format of loading the config
if (UQI_DB_EDITOR_FORM.equals(plugin.getUiComponent())) {
return loadEditorPluginResourceUqi(plugin);
}
}
if ("form.json".equals(resourcePath)) {
// UI config will be available if this plugin is sourced from the cloud
if (plugin.getDatasourceUiConfig() != null) {
return plugin.getDatasourceUiConfig();
}
}
return loadPluginResourceGivenPlugin(plugin, resourcePath);
});

View File

@ -5,7 +5,7 @@ import com.appsmith.external.models.ApiTemplate;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.Property;
import com.appsmith.external.models.TemplateCollection;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.dtos.ActionDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;

View File

@ -8,7 +8,7 @@ import com.appsmith.server.domains.Action;
import com.appsmith.server.domains.ActionCollection;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.CommentThread;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.Organization;

View File

@ -11,7 +11,7 @@ import com.appsmith.server.configurations.CloudServicesConfig;
import com.appsmith.server.constants.Entity;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.constants.Url;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.PluginType;
import com.appsmith.server.dtos.AuthorizationCodeCallbackDTO;

View File

@ -13,7 +13,7 @@ import com.appsmith.server.constants.Entity;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.constants.Resources;
import com.appsmith.server.domains.ApplicationJson;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.NewPage;

View File

@ -10,7 +10,7 @@ import com.appsmith.external.models.Property;
import com.appsmith.external.plugins.PluginExecutor;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.PluginExecutorHelper;

View File

@ -7,7 +7,7 @@ import com.appsmith.external.models.BaseDomain;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationPage;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.Organization;

View File

@ -15,7 +15,7 @@ import com.appsmith.server.constants.SerialiseApplicationObjective;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationJson;
import com.appsmith.server.domains.ApplicationPage;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.PluginType;

View File

@ -0,0 +1,127 @@
package com.appsmith.server.solutions;
import com.appsmith.server.configurations.CloudServicesConfig;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.dtos.ResponseDTO;
import com.appsmith.server.services.ConfigService;
import com.appsmith.server.services.PluginService;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* This class represents a scheduled task that pings cloud services for any updates in available plugins.
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class PluginScheduledTask {
private final ConfigService configService;
private final PluginService pluginService;
private final CloudServicesConfig cloudServicesConfig;
private Instant lastUpdatedAt = null;
/**
* Gets the external IP address of this server and pings a data point to indicate that this server instance is live.
* We use an initial delay of two minutes to roughly wait for the application along with the migrations are finished
* and ready.
*/
// Number of milliseconds between the start of each scheduled calls to this method.
@Scheduled(initialDelay = 2 * 60 * 1000 /* two minutes */, fixedRate = 2 * 60 * 60 * 1000 /* two hours */)
public void updateRemotePlugins() {
// Get all plugins on this instance
final Mono<Map<PluginIdentifier, Plugin>> availablePluginsMono =
pluginService
.getAllRemotePlugins()
.collect(Collectors.toMap(
plugin -> new PluginIdentifier(plugin.getPluginName(), plugin.getVersion()),
plugin -> plugin
));
final Mono<Map<PluginIdentifier, Plugin>> newPluginsMono = getRemotePlugins();
Mono.zip(availablePluginsMono, newPluginsMono)
.flatMapMany(tuple -> {
final Map<PluginIdentifier, Plugin> availablePlugins = tuple.getT1();
final Map<PluginIdentifier, Plugin> newPlugins = tuple.getT2();
final List<Plugin> updatablePlugins = new ArrayList<>();
final List<Plugin> insertablePlugins = new ArrayList<>();
newPlugins.forEach((k, v) -> {
if (availablePlugins.containsKey(k)) {
v.setId(availablePlugins.get(k).getId());
updatablePlugins.add(v);
} else {
insertablePlugins.add(v);
}
});
final Flux<Plugin> updatedPluginsFlux = pluginService.saveAll(updatablePlugins);
final Flux<Organization> organizationFlux = pluginService.saveAll(insertablePlugins)
.filter(Plugin::getDefaultInstall)
.collectList()
.flatMapMany(pluginService::installDefaultPlugins);
return updatedPluginsFlux.zipWith(organizationFlux);
})
.subscribeOn(Schedulers.single())
.subscribe();
}
private Mono<Map<PluginIdentifier, Plugin>> getRemotePlugins() {
final String baseUrl = cloudServicesConfig.getBaseUrl();
if (StringUtils.isEmpty(baseUrl)) {
return Mono.empty();
}
return configService.getInstanceId()
.flatMap(instanceId -> WebClient
.create(
baseUrl + "/api/v1/plugins?instanceId=" + instanceId
+ "&lastUpdatedAt=" + lastUpdatedAt)
.get()
.exchange()
.flatMap(response -> response.bodyToMono(new ParameterizedTypeReference<ResponseDTO<List<Plugin>>>() {
}))
.map(ResponseDTO::getData)
.map(plugins -> {
// Set new updated time
this.lastUpdatedAt = Instant.now();
// Parse plugins into map for easier manipulation
return plugins
.stream()
.collect(Collectors.toMap(
plugin -> new PluginIdentifier(plugin.getPluginName(), plugin.getVersion()),
plugin -> plugin
));
}));
}
@AllArgsConstructor
@Getter
@EqualsAndHashCode
private static class PluginIdentifier {
String pluginName;
String version;
}
}

View File

@ -6,7 +6,7 @@ import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.acl.AppsmithRole;
import com.appsmith.server.domains.ActionCollection;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.Organization;

View File

@ -9,6 +9,7 @@ import com.appsmith.external.helpers.AppsmithEventContext;
import com.appsmith.external.helpers.AppsmithEventContextType;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.Datasource;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.PaginationField;
import com.appsmith.external.models.PaginationType;
@ -21,7 +22,6 @@ import com.appsmith.external.plugins.PluginExecutor;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Datasource;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.Organization;
@ -187,6 +187,7 @@ public class ActionServiceTest {
datasource.setOrganizationId(orgId);
Plugin installed_plugin = pluginRepository.findByPackageName("installed-plugin").block();
datasource.setPluginId(installed_plugin.getId());
datasource.setDatasourceConfiguration(new DatasourceConfiguration());
}
@After

View File

@ -7,7 +7,7 @@ import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationPage;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.GitApplicationMetadata;
import com.appsmith.server.domains.GitAuth;
import com.appsmith.server.domains.Layout;

View File

@ -4,7 +4,7 @@ import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.services.EncryptionService;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.helpers.MockPluginExecutor;

View File

@ -14,7 +14,7 @@ import com.appsmith.external.services.EncryptionService;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.dtos.ActionDTO;

View File

@ -5,7 +5,7 @@ import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.Organization;

View File

@ -4,7 +4,7 @@ import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.PluginType;

View File

@ -7,7 +7,7 @@ import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.Policy;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.User;

View File

@ -9,7 +9,7 @@ import com.appsmith.server.constants.Constraint;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Asset;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.User;
import com.appsmith.server.domains.UserRole;

View File

@ -5,7 +5,7 @@ import com.appsmith.external.models.Policy;
import com.appsmith.external.plugins.PluginExecutor;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.Plugin;

View File

@ -2,7 +2,7 @@ package com.appsmith.server.solutions;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.dtos.ActionDTO;
import com.appsmith.server.helpers.MockPluginExecutor;

View File

@ -6,7 +6,7 @@ import com.appsmith.external.models.OAuth2;
import com.appsmith.external.models.Property;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.User;

View File

@ -10,7 +10,7 @@ import com.appsmith.external.models.DatasourceStructure.TableType;
import com.appsmith.external.models.Property;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.Organization;

View File

@ -14,7 +14,7 @@ import com.appsmith.external.models.UploadedFile;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationPage;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.NewPage;

View File

@ -11,7 +11,7 @@ import com.appsmith.server.constants.SerialiseApplicationObjective;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationJson;
import com.appsmith.server.domains.ApplicationPage;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.GitApplicationMetadata;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.NewAction;

View File

@ -5,7 +5,7 @@ import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.Policy;
import com.appsmith.server.acl.AppsmithRole;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Datasource;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.Plugin;

View File

@ -8,7 +8,7 @@ mvn clean package "$@"
if [ $? -eq 0 ]
then
echo "mvn Successfull"
echo "mvn Successful"
else
echo "mvn Failed"
exit 1