feat(server): Enable upcoming integrations endpoint (#40256)

This PR adds a new endpoint to the Plugin API that fetches upcoming
integrations from Appsmith's cloud services. This feature enables users
to discover and prepare for new integration options that will be
available in future releases.

## Changes
- Added a new getUpcomingIntegrations endpoint to the PluginController
at GET /api/v1/plugins/upcoming-integrations
- Implemented the corresponding service method in PluginService to fetch
data from cloud services
- Added proper error handling for cloud service communication issues
- Injected CloudServicesConfig to ensure proper cloud service URLs are
used
- Added instanceId to all requests to cloud services for tracking and
authorization

## Technical Details
- Uses WebClientUtils for HTTP requests to external services
- Uses ConfigService to retrieve the instance ID for authentication
- Handles error responses gracefully, returning empty results instead of
errors to clients
- Returns properly formatted integration data including names,
descriptions, and other metadata

This enhancement will help users discover upcoming integration options
and provide feedback on planned datasource connectors.

/ok-to-test tags="@tag.Sanity"

Solves https://github.com/appsmithorg/appsmith/issues/40048

<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/14490650726>
> Commit: a9ae87419b119adb08a156bf19dbe9fe7dd16b64
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=14490650726&attempt=2"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Sanity`
> Spec:
> <hr>Wed, 16 Apr 2025 11:50:36 UTC
<!-- end of auto-generated comment: Cypress test results  -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added a new endpoint to fetch upcoming integrations, providing users
with information about future external service integrations.
- **Bug Fixes**
- Improved error handling for external integration data fetches,
ensuring a graceful fallback and user-friendly messaging if data cannot
be retrieved.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: nilansh <nilansh@appsmith.com>
This commit is contained in:
vivek-appsmith 2025-04-16 17:27:17 +05:30 committed by GitHub
parent e07de53491
commit 99e3cb5d65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 106 additions and 6 deletions

View File

@ -1,5 +1,6 @@
package com.appsmith.server.controllers;
import com.appsmith.server.configurations.CloudServicesConfig;
import com.appsmith.server.constants.Url;
import com.appsmith.server.controllers.ce.PluginControllerCE;
import com.appsmith.server.plugins.base.PluginService;
@ -11,7 +12,10 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping(Url.PLUGIN_URL)
public class PluginController extends PluginControllerCE {
public PluginController(PluginService service, PluginTriggerSolution pluginTriggerSolution) {
super(service, pluginTriggerSolution);
public PluginController(
PluginService service,
PluginTriggerSolution pluginTriggerSolution,
CloudServicesConfig cloudServicesConfig) {
super(service, pluginTriggerSolution, cloudServicesConfig);
}
}

View File

@ -3,10 +3,12 @@ package com.appsmith.server.controllers.ce;
import com.appsmith.external.models.TriggerRequestDTO;
import com.appsmith.external.models.TriggerResultDTO;
import com.appsmith.external.views.Views;
import com.appsmith.server.configurations.CloudServicesConfig;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.constants.Url;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.dtos.ResponseDTO;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.plugins.base.PluginService;
import com.appsmith.server.plugins.solutions.PluginTriggerSolution;
import com.fasterxml.jackson.annotation.JsonView;
@ -28,6 +30,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
@RequestMapping(Url.PLUGIN_URL)
@RequiredArgsConstructor
@ -38,6 +41,8 @@ public class PluginControllerCE {
private final PluginTriggerSolution pluginTriggerSolution;
private final CloudServicesConfig cloudServicesConfig;
@JsonView(Views.Public.class)
@GetMapping
public Mono<ResponseDTO<List<Plugin>>> getAll(@RequestParam String workspaceId) {
@ -96,4 +101,21 @@ public class PluginControllerCE {
serverWebExchange.getRequest().getHeaders())
.map(triggerResultDTO -> new ResponseDTO<>(HttpStatus.OK, triggerResultDTO));
}
@JsonView(Views.Public.class)
@GetMapping("/upcoming-integrations")
public Mono<ResponseDTO<List<Map<String, String>>>> getUpcomingIntegrations() {
log.debug("Fetching upcoming integrations from external API");
return service.getUpcomingIntegrations()
.map(integrations -> new ResponseDTO<>(HttpStatus.OK, integrations))
.onErrorResume(error -> {
if (error instanceof AppsmithException) {
log.warn("Cloud service error: {}", error.getMessage());
return Mono.just(new ResponseDTO<>(HttpStatus.OK.value(), List.of(), error.getMessage()));
}
log.warn("Error retrieving upcoming integrations from external service: {}", error.getMessage());
return Mono.just(new ResponseDTO<>(
HttpStatus.OK.value(), List.of(), "Unable to fetch upcoming integrations at this time"));
});
}
}

View File

@ -53,4 +53,6 @@ public interface PluginServiceCE extends CrudService<Plugin, String> {
Mono<Map<String, Plugin>> findAllPluginsInWorkspace(String workspaceId);
Flux<Plugin> getPluginsByType(PluginType pluginType);
Mono<List<Map<String, String>>> getUpcomingIntegrations();
}

View File

@ -2,6 +2,7 @@ package com.appsmith.server.plugins.base;
import com.appsmith.external.models.Datasource;
import com.appsmith.external.models.PluginType;
import com.appsmith.server.configurations.CloudServicesConfig;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.Workspace;
@ -15,7 +16,9 @@ import com.appsmith.server.helpers.LoadShifter;
import com.appsmith.server.repositories.PluginRepository;
import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.BaseService;
import com.appsmith.server.services.ConfigService;
import com.appsmith.server.services.WorkspaceService;
import com.appsmith.util.WebClientUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
@ -46,6 +49,7 @@ import java.io.InputStream;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@ -65,6 +69,7 @@ public class PluginServiceCEImpl extends BaseService<PluginRepository, Plugin, S
private final ReactiveRedisTemplate<String, String> reactiveTemplate;
private final ChannelTopic topic;
private final ObjectMapper objectMapper;
private final CloudServicesConfig cloudServicesConfig;
private final Map<String, Mono<Map<?, ?>>> formCache = new HashMap<>();
private final Map<String, Mono<Map<String, String>>> templateCache = new HashMap<>();
@ -86,6 +91,8 @@ public class PluginServiceCEImpl extends BaseService<PluginRepository, Plugin, S
public static final String KEY_COMMENT = "_comment";
public static final String KEY_FILES = "files";
private final ConfigService configService;
@Autowired
public PluginServiceCEImpl(
Validator validator,
@ -95,13 +102,17 @@ public class PluginServiceCEImpl extends BaseService<PluginRepository, Plugin, S
PluginManager pluginManager,
ReactiveRedisTemplate<String, String> reactiveTemplate,
ChannelTopic topic,
ObjectMapper objectMapper) {
ObjectMapper objectMapper,
CloudServicesConfig cloudServicesConfig,
ConfigService configService) {
super(validator, repository, analyticsService);
this.workspaceService = workspaceService;
this.pluginManager = pluginManager;
this.reactiveTemplate = reactiveTemplate;
this.topic = topic;
this.objectMapper = objectMapper;
this.cloudServicesConfig = cloudServicesConfig;
this.configService = configService;
}
@Override
@ -660,6 +671,55 @@ public class PluginServiceCEImpl extends BaseService<PluginRepository, Plugin, S
});
}
@Override
public Mono<List<Map<String, String>>> getUpcomingIntegrations() {
log.debug("Fetching upcoming integrations from external API");
return configService.getInstanceId().flatMap(instanceId -> {
String apiUrl = cloudServicesConfig.getBaseUrl()
+ "/api/v1/config/external-saas/upcoming-integrations?instanceId=" + instanceId;
return WebClientUtils.create()
.get()
.uri(apiUrl)
.retrieve()
.bodyToMono(Map.class)
.flatMap(response -> {
// Extract the integrations list from the response
if (response.containsKey("data")) {
List<Map<String, String>> integrations = new ArrayList<>();
List<?> data = (List<?>) response.get("data");
for (Object item : data) {
if (item instanceof Map) {
integrations.add((Map<String, String>) item);
}
}
return Mono.just(integrations);
} else if (response.containsKey("responseMeta")) {
Map<String, Object> responseMeta = (Map<String, Object>) response.get("responseMeta");
if (responseMeta.containsKey("error")) {
Map<String, Object> error = (Map<String, Object>) responseMeta.get("error");
if (error.containsKey("message")) {
String errorMessage = (String) error.get("message");
return Mono.error(new AppsmithException(
AppsmithError.INSTANCE_REGISTRATION_FAILURE, errorMessage));
}
}
return Mono.error(new RuntimeException("Unknown error in response metadata"));
}
return Mono.just(List.<Map<String, String>>of());
})
.onErrorResume(error -> {
if (error instanceof AppsmithException) {
return Mono.error(error);
}
log.warn(
"Error retrieving upcoming integrations from external service: {}", error.getMessage());
return Mono.error(
new RuntimeException("Error retrieving upcoming integrations: " + error.getMessage()));
});
});
}
@Data
static class PluginTemplatesMeta {
List<PluginTemplate> templates;

View File

@ -1,7 +1,9 @@
package com.appsmith.server.plugins.base;
import com.appsmith.server.configurations.CloudServicesConfig;
import com.appsmith.server.repositories.PluginRepository;
import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.ConfigService;
import com.appsmith.server.services.WorkspaceService;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.validation.Validator;
@ -23,7 +25,9 @@ public class PluginServiceImpl extends PluginServiceCEImpl implements PluginServ
PluginManager pluginManager,
ReactiveRedisTemplate<String, String> reactiveTemplate,
ChannelTopic topic,
ObjectMapper objectMapper) {
ObjectMapper objectMapper,
CloudServicesConfig cloudServicesConfig,
ConfigService configService) {
super(
validator,
@ -33,6 +37,8 @@ public class PluginServiceImpl extends PluginServiceCEImpl implements PluginServ
pluginManager,
reactiveTemplate,
topic,
objectMapper);
objectMapper,
cloudServicesConfig,
configService);
}
}

View File

@ -1,10 +1,12 @@
package com.appsmith.server.services.ce;
import com.appsmith.server.configurations.CloudServicesConfig;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.plugins.base.PluginServiceCE;
import com.appsmith.server.plugins.base.PluginServiceCEImpl;
import com.appsmith.server.repositories.PluginRepository;
import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.ConfigService;
import com.appsmith.server.services.WorkspaceService;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.validation.Validator;
@ -56,6 +58,8 @@ public class PluginServiceCEImplTest {
ObjectMapper objectMapper;
PluginServiceCE pluginService;
CloudServicesConfig cloudServicesConfig;
ConfigService configService;
@BeforeEach
public void setUp() {
@ -68,7 +72,9 @@ public class PluginServiceCEImplTest {
pluginManager,
reactiveTemplate,
topic,
objectMapper);
objectMapper,
cloudServicesConfig,
configService);
}
@Test