Add server-side templates support for plugins

This commit is contained in:
Shrikant Kandula 2020-06-24 11:08:25 +00:00 committed by Arpit Mohan
parent 5ce19962a3
commit eee2cfcaff
13 changed files with 175 additions and 33 deletions

View File

@ -0,0 +1,12 @@
{
"insert": "users",
"documents": [
{
"name": "John Smith",
"email": [
"john@appsmith.com"
],
"gender": "M"
}
]
}

View File

@ -0,0 +1,10 @@
{
"delete": "users",
"deletes": [
{
"q": {
"id": 10
}
}
]
}

View File

@ -0,0 +1,12 @@
{
"find": "users",
"filter": {
"id": {
"$gte": 10
}
},
"sort": {
"id": 1
},
"limit": 10
}

View File

@ -0,0 +1,16 @@
{
"update": "users",
"updates": [
{
"q": {
"id": 10
},
"u": {
"name": "Updated Sam",
"email": [
"updates@appsmith.com"
]
}
}
]
}

View File

@ -119,8 +119,7 @@
}, },
{ {
"label": "Certificate", "label": "Certificate",
"configProperty": "configProperty": "datasourceConfiguration.connection.ssl.certificateFile",
"datasourceConfiguration.connection.ssl.certificateFile",
"controlType": "FILE_PICKER" "controlType": "FILE_PICKER"
} }
] ]
@ -130,20 +129,17 @@
"children": [ "children": [
{ {
"label": "CA Certificate", "label": "CA Certificate",
"configProperty": "configProperty": "datasourceConfiguration.connection.ssl.caCertificateFile",
"datasourceConfiguration.connection.ssl.caCertificateFile",
"controlType": "FILE_PICKER" "controlType": "FILE_PICKER"
}, },
{ {
"label": "PEM Certificate", "label": "PEM Certificate",
"configProperty": "configProperty": "datasourceConfiguration.connection.ssl.pemCertificate.file",
"datasourceConfiguration.connection.ssl.pemCertificate.file",
"controlType": "FILE_PICKER" "controlType": "FILE_PICKER"
}, },
{ {
"label": "PEM Passphrase", "label": "PEM Passphrase",
"configProperty": "configProperty": "datasourceConfiguration.connection.ssl.pemCertificate.password",
"datasourceConfiguration.connection.ssl.pemCertificate.password",
"dataType": "PASSWORD", "dataType": "PASSWORD",
"controlType": "INPUT_TEXT", "controlType": "INPUT_TEXT",
"placeholderText": "PEM Passphrase" "placeholderText": "PEM Passphrase"

View File

@ -0,0 +1,4 @@
INSERT INTO users
(id, name, gender, avatar, email, address, role)
VALUES
(?, ?, ?, ?, ?, ?, ?);

View File

@ -0,0 +1 @@
DELETE FROM users WHERE id = ?;

View File

@ -0,0 +1 @@
SELECT * FROM users ORDER BY id LIMIT 10;

View File

@ -0,0 +1,3 @@
UPDATE users
SET status = 'APPROVED'
WHERE id = 1;

View File

@ -6,9 +6,11 @@ import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import net.minidev.json.annotate.JsonIgnore; import net.minidev.json.annotate.JsonIgnore;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Document;
import java.util.List; import java.util.List;
import java.util.Map;
@Getter @Getter
@Setter @Setter
@ -44,4 +46,7 @@ public class Plugin extends BaseDomain {
Boolean allowUserDatasources = true; Boolean allowUserDatasources = true;
@Transient
Map<String, String> templates;
} }

View File

@ -44,6 +44,7 @@ public enum AppsmithError {
PLUGIN_RUN_FAILED(500, 5003, "Plugin execution failed with error {0}"), PLUGIN_RUN_FAILED(500, 5003, "Plugin execution failed with error {0}"),
PLUGIN_EXECUTION_TIMEOUT(504, 5040, "Plugin Execution exceeded the maximum allowed time. Please increase the timeout in your action settings or check your backend action endpoint"), PLUGIN_EXECUTION_TIMEOUT(504, 5040, "Plugin Execution exceeded the maximum allowed time. Please increase the timeout in your action settings or check your backend action endpoint"),
PLUGIN_LOAD_FORM_JSON_FAIL(500, 5004, "Unable to load datasource form configuration. Details: {0}."), PLUGIN_LOAD_FORM_JSON_FAIL(500, 5004, "Unable to load datasource form configuration. Details: {0}."),
PLUGIN_LOAD_TEMPLATES_FAIL(500, 5005, "Unable to load datasource templates. Details: {0}."),
MARKETPLACE_TIMEOUT(504, 5041, "Marketplace is responding too slowly. Please try again later"), MARKETPLACE_TIMEOUT(504, 5041, "Marketplace is responding too slowly. Please try again later"),
DATASOURCE_HAS_ACTIONS(409, 4030, "Cannot delete datasource since it has {0} action(s) using it."), DATASOURCE_HAS_ACTIONS(409, 4030, "Cannot delete datasource since it has {0} action(s) using it."),
; ;

View File

@ -8,6 +8,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.io.InputStream; import java.io.InputStream;
import java.util.Map;
public interface PluginService extends CrudService<Plugin, String> { public interface PluginService extends CrudService<Plugin, String> {
@ -25,7 +26,7 @@ public interface PluginService extends CrudService<Plugin, String> {
Plugin redisInstallPlugin(InstallPluginRedisDTO installPluginRedisDTO); Plugin redisInstallPlugin(InstallPluginRedisDTO installPluginRedisDTO);
Mono<Object> getFormConfig(String pluginId); Mono<Map> getFormConfig(String pluginId);
Mono<InputStream> getPluginResource(String pluginId, String resourcePath); Mono<InputStream> loadPluginResource(String pluginId, String resourcePath);
} }

View File

@ -17,7 +17,8 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.pf4j.PluginManager; import org.pf4j.PluginManager;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext; import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate; import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Criteria;
@ -26,17 +27,23 @@ import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic; import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
import reactor.core.Exceptions;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Scheduler;
import javax.validation.Validator; import javax.validation.Validator;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -45,13 +52,14 @@ import java.util.stream.Collectors;
@Service @Service
public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, String> implements PluginService { public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, String> implements PluginService {
private final ApplicationContext applicationContext;
private final OrganizationService organizationService; private final OrganizationService organizationService;
private final PluginManager pluginManager; private final PluginManager pluginManager;
private final ReactiveRedisTemplate<String, String> reactiveTemplate; private final ReactiveRedisTemplate<String, String> reactiveTemplate;
private final ChannelTopic topic; private final ChannelTopic topic;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final SessionUserService sessionUserService;
private final Map<String, Mono<Map>> formCache = new HashMap<>();
private final Map<String, Mono<Map<String, String>>> templateCache = new HashMap<>();
private static final int CONNECTION_TIMEOUT = 10000; private static final int CONNECTION_TIMEOUT = 10000;
private static final int READ_TIMEOUT = 10000; private static final int READ_TIMEOUT = 10000;
@ -63,21 +71,17 @@ public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, Str
ReactiveMongoTemplate reactiveMongoTemplate, ReactiveMongoTemplate reactiveMongoTemplate,
PluginRepository repository, PluginRepository repository,
AnalyticsService analyticsService, AnalyticsService analyticsService,
ApplicationContext applicationContext,
OrganizationService organizationService, OrganizationService organizationService,
PluginManager pluginManager, PluginManager pluginManager,
ReactiveRedisTemplate<String, String> reactiveTemplate, ReactiveRedisTemplate<String, String> reactiveTemplate,
ChannelTopic topic, ChannelTopic topic,
ObjectMapper objectMapper, ObjectMapper objectMapper) {
SessionUserService sessionUserService) {
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService); super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService);
this.applicationContext = applicationContext;
this.organizationService = organizationService; this.organizationService = organizationService;
this.pluginManager = pluginManager; this.pluginManager = pluginManager;
this.reactiveTemplate = reactiveTemplate; this.reactiveTemplate = reactiveTemplate;
this.topic = topic; this.topic = topic;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.sessionUserService = sessionUserService;
} }
@Override @Override
@ -96,12 +100,12 @@ public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, Str
log.debug("Fetching plugins by params: {} for org: {}", params, org.getName()); log.debug("Fetching plugins by params: {} for org: {}", params, org.getName());
if (org.getPlugins() == null) { if (org.getPlugins() == null) {
log.debug("Null installed plugins found for org: {}. Return empty plugins", org.getName()); log.debug("Null installed plugins found for org: {}. Return empty plugins", org.getName());
return Flux.fromIterable(new ArrayList<>()); return Flux.empty();
} }
List<String> pluginIds = org.getPlugins() List<String> pluginIds = org.getPlugins()
.stream() .stream()
.map(obj -> obj.getPluginId()) .map(OrganizationPlugin::getPluginId)
.collect(Collectors.toList()); .collect(Collectors.toList());
Query query = new Query(); Query query = new Query();
query.addCriteria(Criteria.where(FieldName.ID).in(pluginIds)); query.addCriteria(Criteria.where(FieldName.ID).in(pluginIds));
@ -112,12 +116,17 @@ public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, Str
query.addCriteria(Criteria.where(FieldName.TYPE).is(pluginType)); query.addCriteria(Criteria.where(FieldName.TYPE).is(pluginType));
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
log.error("No plugins for type : {}", params.getFirst(FieldName.TYPE)); log.error("No plugins for type : {}", params.getFirst(FieldName.TYPE));
List<Plugin> emptyPlugins = new ArrayList<>(); return Flux.empty();
return Flux.fromIterable(emptyPlugins);
} }
} }
return mongoTemplate.find(query, Plugin.class); return mongoTemplate.find(query, Plugin.class);
}); })
.flatMap(plugin ->
getTemplates(plugin)
.doOnSuccess(plugin::setTemplates)
.thenReturn(plugin)
);
} }
@Override @Override
@ -285,19 +294,90 @@ public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, Str
} }
@Override @Override
public Mono<Object> getFormConfig(String pluginId) { public Mono<Map> getFormConfig(String pluginId) {
return getPluginResource(pluginId, "form.json") if (!formCache.containsKey(pluginId)) {
.flatMap(jsonStream -> { final Mono<Map> mono = loadPluginResource(pluginId, "form.json")
try { .flatMap(jsonStream -> Mono.fromSupplier(() -> {
return Mono.just(new ObjectMapper().readValue(jsonStream, Map.class)); try {
} catch (IOException e) { return new ObjectMapper().readValue(jsonStream, Map.class);
return Mono.error(new AppsmithException(AppsmithError.PLUGIN_LOAD_FORM_JSON_FAIL, e.getMessage())); } catch (IOException e) {
} log.error("Error loading form JSON for plugin " + pluginId, e);
}); throw Exceptions.propagate(
new AppsmithException(AppsmithError.PLUGIN_LOAD_FORM_JSON_FAIL, e.getMessage())
);
}
}))
.doOnError(throwable ->
// Remove this pluginId from the cache so it is tried again next time.
formCache.remove(pluginId)
)
.onErrorMap(Exceptions::unwrap)
.cache();
formCache.put(pluginId, mono);
}
return formCache.get(pluginId);
}
private Mono<Map<String, String>> getTemplates(Plugin plugin) {
final String pluginId = plugin.getId();
if (!templateCache.containsKey(pluginId)) {
final Mono<Map<String, String>> mono = Mono.fromSupplier(() -> loadTemplatesFromPlugin(plugin))
.onErrorReturn(FileNotFoundException.class, Collections.emptyMap())
.doOnError(throwable ->
// Remove this pluginId from the cache so it is tried again next time.
templateCache.remove(pluginId)
)
// It's okay if the templates folder is not present, we just return empty templates collection.
.onErrorMap(throwable -> new AppsmithException(
AppsmithError.PLUGIN_LOAD_TEMPLATES_FAIL, Exceptions.unwrap(throwable).getMessage())
)
.cache();
templateCache.put(pluginId, mono);
}
return templateCache.get(pluginId);
}
private Map<String, String> loadTemplatesFromPlugin(Plugin plugin) {
final PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(
pluginManager
.getPlugin(plugin.getPackageName())
.getPluginClassLoader()
);
final Map<String, String> templates = new HashMap<>();
Resource[] resources;
try {
resources = resolver.getResources("templates/*");
} catch (IOException e) {
log.error("Error resolving templates in plugin for id: " + plugin.getId());
throw Exceptions.propagate(e);
}
for (final Resource resource : resources) {
final String filename = resource.getFilename();
try {
final String templateContent = StreamUtils.copyToString(
resource.getInputStream(), Charset.defaultCharset());
if (filename != null) {
templates.put(filename.replaceFirst("\\.\\w+$", ""), templateContent);
}
} catch (IOException e) {
log.error("Error loading template " + filename + " for plugin " + plugin.getId());
throw Exceptions.propagate(e);
}
}
return templates;
} }
@Override @Override
public Mono<InputStream> getPluginResource(String pluginId, String resourcePath) { public Mono<InputStream> loadPluginResource(String pluginId, String resourcePath) {
return findById(pluginId) return findById(pluginId)
.flatMap(plugin -> { .flatMap(plugin -> {
InputStream formResourceStream = pluginManager InputStream formResourceStream = pluginManager