Adding query parameters to filter get API calls. Specifically adding filter by plugin type in the get plugins API.

This commit is contained in:
Arpit Mohan 2019-11-27 10:51:43 +00:00
parent dea2efa776
commit fdb2f7a25d
27 changed files with 91 additions and 213 deletions

View File

@ -41,7 +41,7 @@ public class SecurityConfig {
* This routerFunction is required to map /public/** endpoints to the src/main/resources/public folder
* This is to allow static resources to be served by the server. Couldn't find an easier way to do this,
* hence using RouterFunctions to implement this feature.
*
* <p>
* Future folks: Please check out links:
* - https://www.baeldung.com/spring-webflux-static-content
* - https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-config-static-resources
@ -80,7 +80,7 @@ public class SecurityConfig {
user.setEmail("api_user");
user.setName("api_user");
user.setPassword(passwordEncoder().encode("8uA@;&mB:cnvN~{#"));
user.setRoles(Set.of(new Role(Security.USER_ROLE.toString())));
user.setRoles(Set.of(new Role(Security.USER_ROLE)));
return new MapReactiveUserDetailsService(user);
}

View File

@ -11,4 +11,5 @@ public class FieldName {
public static String CONFIG = "config";
public static String PLUGIN = "plugin";
public static String DEFAULT_PAGE_NAME = "Home";
public static String TYPE = "type";
}

View File

@ -7,16 +7,19 @@ import com.appsmith.server.services.CrudService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import reactor.core.publisher.Mono;
import javax.validation.Valid;
import java.util.List;
@RequiredArgsConstructor
@Slf4j
@ -33,9 +36,9 @@ public abstract class BaseController<S extends CrudService, T extends BaseDomain
}
@GetMapping("")
public Mono<ResponseDTO<T>> getAll() {
public Mono<ResponseDTO<List<T>>> getAll(@RequestParam MultiValueMap<String, String> params) {
log.debug("Going to get all resources");
return service.get().collectList()
return service.get(params).collectList()
.map(resources -> new ResponseDTO<>(HttpStatus.OK.value(), resources, null));
}

View File

@ -8,14 +8,18 @@ import com.appsmith.server.dtos.ResponseDTO;
import com.appsmith.server.services.PluginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import javax.validation.Valid;
import java.util.List;
@RestController
@ -41,4 +45,10 @@ public class PluginController extends BaseController<PluginService, Plugin, Stri
.map(organization -> new ResponseDTO<>(HttpStatus.CREATED.value(), organization, null));
}
@GetMapping("")
@Override
public Mono<ResponseDTO<List<Plugin>>> getAll(@RequestParam MultiValueMap<String, String> params) {
return service.get(params).collectList()
.map(resources -> new ResponseDTO<>(HttpStatus.OK.value(), resources, null));
}
}

View File

@ -18,6 +18,7 @@ public class Plugin extends BaseDomain {
String name;
@Indexed
PluginType type;
@Indexed(unique = true)

View File

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

View File

@ -1,7 +1,7 @@
package com.appsmith.server.dtos;
public interface PageNameIdDTO {
public String getId();
String getId();
public String getName();
String getName();
}

View File

@ -39,7 +39,7 @@ public enum AppsmithError {
private Integer appErrorCode;
private String message;
private AppsmithError(Integer httpErrorCode, Integer appErrorCode, String message, Object... args) {
AppsmithError(Integer httpErrorCode, Integer appErrorCode, String message, Object... args) {
this.httpErrorCode = httpErrorCode;
this.appErrorCode = appErrorCode;
MessageFormat fmt = new MessageFormat(message);

View File

@ -30,10 +30,10 @@ public class AclFilter implements WebFilter {
* The parameters are:
* 1. HTTP Method - GET, POST etc.
* 2. Resource being accessed - layouts, pages , etc
*
* <p>
* The ACL policy filters user access based on the permissions that the user has and the resource they are trying
* to access
*
* <p>
* Check @see src/main/resources/acl.rego for details of a sample ACL policy
*
* @param exchange

View File

@ -1,77 +0,0 @@
package com.appsmith.server.plugins;
import com.appsmith.server.domains.OldProperty;
import com.appsmith.server.domains.Query;
import com.appsmith.server.dtos.CommandQueryParams;
import com.appsmith.server.services.OldPluginExecutor;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONObject;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Component
@Slf4j
public class RestTemplateOldPluginExecutor extends OldPluginExecutor {
final String PROP_URL = "url";
final String PROP_HTTP_METHOD = "method";
@Override
protected Flux<Object> execute(Query query, CommandQueryParams params) {
String requestBody = query.getCommandTemplate();
Map<String, OldProperty> propertyMap = query.getProperties()
.stream()
.collect(Collectors.toMap(OldProperty::getKey, prop -> prop));
String url = propertyMap.get(PROP_URL).getValue();
String httpMethod = propertyMap.get(PROP_HTTP_METHOD).getValue();
WebClient webClient = WebClient.builder()
.baseUrl(url)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
WebClient.RequestHeadersSpec<?> request = webClient.method(HttpMethod.resolve(httpMethod))
.body(BodyInserters.fromObject(requestBody));
Mono<ClientResponse> responseMono = request.exchange();
ClientResponse clientResponse = responseMono.block();
List<String> contentTypes = clientResponse.headers().header(HttpHeaders.CONTENT_TYPE);
Class clazz = String.class;
if (contentTypes != null && contentTypes.size() > 0) {
String contentType = contentTypes.get(0);
boolean isJson = MediaType.APPLICATION_JSON_UTF8_VALUE.toLowerCase()
.equals(contentType.toLowerCase()
.replaceAll("\\s", ""))
|| MediaType.APPLICATION_JSON_VALUE.equals(contentType.toLowerCase());
if (isJson) {
clazz = JSONObject.class;
}
}
return clientResponse.bodyToFlux(clazz);
}
@Override
protected void init() {
log.debug("In the RestTemplatePluginExecutor init()");
}
@Override
protected void destroy() {
log.debug("In the RestTemplatePluginExecutor destroy()");
}
}

View File

@ -2,7 +2,6 @@ package com.appsmith.server.repositories;
import com.appsmith.server.domains.Action;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
@ -10,7 +9,5 @@ public interface ActionRepository extends BaseRepository<Action, String> {
Mono<Action> findById(String id);
Flux<Action> findAllByOrderByName();
Mono<Action> findByName(String name);
}

View File

@ -2,12 +2,10 @@ package com.appsmith.server.repositories;
import com.appsmith.server.domains.Application;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
public interface ApplicationRepository extends BaseRepository<Application, String> {
Flux<Application> findByOrganizationId(String orgId);
Mono<Application> findByIdAndOrganizationId(String id, String orgId);

View File

@ -1,11 +1,11 @@
package com.appsmith.server.repositories;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import java.io.Serializable;
@NoRepositoryBean
public interface BaseRepository<T, ID extends Serializable> extends ReactiveCrudRepository<T, ID> {
public interface BaseRepository<T, ID extends Serializable> extends ReactiveMongoRepository<T, ID> {
}

View File

@ -1,7 +1,9 @@
package com.appsmith.server.repositories;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.PluginType;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
@ -9,4 +11,6 @@ public interface PluginRepository extends BaseRepository<Plugin, String> {
Mono<Plugin> findByName(String name);
Mono<Plugin> findById(String id);
Flux<Plugin> findByType(PluginType pluginType);
}

View File

@ -24,9 +24,13 @@ import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
@ -463,7 +467,12 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
}
@Override
public Flux<Action> get() {
return repository.findAllByOrderByName();
public Flux<Action> get(MultiValueMap<String, String> params) {
Action actionExample = new Action();
if (params.getFirst(FieldName.NAME) != null) {
actionExample.setName(params.getFirst(FieldName.NAME));
}
Sort sort = new Sort(Direction.ASC, FieldName.NAME );
return repository.findAll(Example.of(actionExample), sort);
}
}

View File

@ -29,7 +29,7 @@ public class ApplicationPageServiceImpl implements ApplicationPageService {
public Mono<Page> createPage(Page page) {
if (page.getId() != null) {
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ID));
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ID));
} else if (page.getName() == null) {
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.NAME));
} else if (page.getApplicationId() == null) {

View File

@ -11,9 +11,11 @@ import com.appsmith.server.repositories.ApplicationRepository;
import com.appsmith.server.repositories.PageRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
@ -48,12 +50,16 @@ public class ApplicationServiceImpl extends BaseService<ApplicationRepository, A
}
@Override
public Flux<Application> get() {
public Flux<Application> get(MultiValueMap<String, String> params) {
Mono<User> userMono = sessionUserService.getCurrentUser();
return userMono
.map(user -> user.getCurrentOrganizationId())
.flatMapMany(orgId -> repository.findByOrganizationId(orgId));
.flatMapMany(orgId -> {
Application applicationExample = new Application();
applicationExample.setOrganizationId(orgId);
return repository.findAll(Example.of(applicationExample));
});
}
@Override

View File

@ -13,6 +13,7 @@ import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.util.MultiValueMap;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
@ -67,7 +68,9 @@ public abstract class BaseService<R extends BaseRepository, T extends BaseDomain
}
@Override
public Flux<T> get() {
public Flux<T> get(MultiValueMap<String, String> params) {
// In the base service we aren't handling the query parameters. In order to filter records using the query params,
// each service must implement it for their usecase. Need to come up with a better strategy for doing this.
return repository.findAll();
}

View File

@ -1,12 +1,13 @@
package com.appsmith.server.services;
import com.appsmith.server.domains.BaseDomain;
import org.springframework.util.MultiValueMap;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface CrudService<T extends BaseDomain, ID> {
Flux<T> get();
Flux<T> get(MultiValueMap<String, String> params);
Mono<T> create(T resource);

View File

@ -16,6 +16,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
@ -169,7 +170,8 @@ public class DatasourceServiceImpl extends BaseService<DatasourceRepository, Dat
}
@Override
public Flux<Datasource> get() {
public Flux<Datasource> get(MultiValueMap<String, String> params) {
return sessionUserService
.getCurrentUser()
.flatMapMany(user -> repository.findAllByOrganizationId(user.getCurrentOrganizationId()));

View File

@ -88,7 +88,7 @@ public class LayoutServiceImpl implements LayoutService {
@Override
public Mono<Layout> updateLayout(String pageId, String layoutId, Layout layout) {
List<String> mustacheKeys = new ArrayList<>();;
List<String> mustacheKeys = new ArrayList<>();
//Extract the mustache keys and find all keys which match actions
JSONObject dsl = layout.getDsl();
try {
@ -101,7 +101,7 @@ public class LayoutServiceImpl implements LayoutService {
Mono<Set<String>> actionsInPage = Flux.fromIterable(mustacheKeys)
.map(mustacheKey -> {
String subStrings[] = mustacheKey.split(Pattern.quote("."));
String[] subStrings = mustacheKey.split(Pattern.quote("."));
// Assumption here is that the action name would always be the first substring here.
// If we start referring to actions from another page via <PageName>.<ActionName> format, this
// would break.

View File

@ -1,78 +0,0 @@
package com.appsmith.server.services;
import com.appsmith.server.domains.Query;
import com.appsmith.server.dtos.CommandQueryParams;
import com.appsmith.server.dtos.OldParam;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import reactor.core.publisher.Flux;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
public abstract class OldPluginExecutor {
/**
* This function executes the command against a backend datasource.
* All the variables in commandTemplate of the Query object has already been replaced with actual values
* by the #replaceTemplate() function
*
* @param query
* @param params
* @return Flux<Object>
*/
protected abstract Flux<Object> execute(Query query, CommandQueryParams params);
/**
* This function should be run when the plugin is initialized
*/
protected abstract void init();
/**
* This function should be run when the plugin is destroyed
*/
protected abstract void destroy();
/**
* This function replaces the variables in the query commandTemplate with the actual params
* Executors can override this function to provide their own implementation if they wish to do something custom
*
* @param query Query
* @param params CommandQueryParams
*/
protected Query replaceTemplate(Query query, CommandQueryParams params) {
MustacheFactory mf = new DefaultMustacheFactory();
Mustache mustache = mf.compile(new StringReader(query.getCommandTemplate()), "commandTemplate");
Writer writer = new StringWriter();
Map<String, String> queryMap = new HashMap<>();
Map<String, String> headerMap = new HashMap<>();
if (params.getQueryOldParams() != null) {
queryMap = params
.getQueryOldParams()
.stream()
.collect(
Collectors.toMap(OldParam::getKey, OldParam::getValue,
// Incase there's a conflict, we pick the older value
(oldValue, newValue) -> oldValue));
}
if (params.getHeaderOldParams() != null) {
headerMap = params
.getHeaderOldParams()
.stream()
.collect(
Collectors.toMap(OldParam::getKey, OldParam::getValue,
// Incase there's a conflict, we pick the older value
(oldValue, newValue) -> oldValue));
}
mustache.execute(writer, queryMap);
query.setCommandTemplate(writer.toString());
return query;
}
}

View File

@ -2,23 +2,12 @@ package com.appsmith.server.services;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.PluginType;
import com.appsmith.server.dtos.InstallPluginRedisDTO;
import com.appsmith.server.dtos.PluginOrgDTO;
import reactor.core.publisher.Mono;
public interface PluginService extends CrudService<Plugin, String> {
/**
* Return an instance of PluginExecutor based on the classname available.
* If the classname is not available, null is returned.
*
* @param pluginType
* @param className
* @return PluginExecutor
*/
OldPluginExecutor getPluginExecutor(PluginType pluginType, String className);
Mono<Organization> installPlugin(PluginOrgDTO plugin);
Mono<Organization> uninstallPlugin(PluginOrgDTO plugin);

View File

@ -1,5 +1,6 @@
package com.appsmith.server.services;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.OrganizationPlugin;
import com.appsmith.server.domains.Plugin;
@ -18,11 +19,14 @@ import org.apache.commons.io.FileUtils;
import org.pf4j.PluginManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.domain.Example;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
@ -72,15 +76,20 @@ public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, Str
this.sessionUserService = sessionUserService;
}
public OldPluginExecutor getPluginExecutor(PluginType pluginType, String className) {
Class<?> clazz;
try {
clazz = Class.forName(className);
return (OldPluginExecutor) applicationContext.getBean(clazz);
} catch (ClassNotFoundException e) {
log.error("Unable to find class {}. ", className, e);
@Override
public Flux<Plugin> get(MultiValueMap<String, String> params) {
log.debug("Going to filter plugins by params: {}", params);
Plugin examplePlugin = new Plugin();
if (params.getFirst(FieldName.TYPE) != null) {
try {
PluginType pluginType = PluginType.valueOf(params.getFirst(FieldName.TYPE));
examplePlugin.setType(pluginType);
} catch(IllegalArgumentException e) {
log.error("No plugins for type : {}", params.getFirst(FieldName.TYPE));
return Flux.empty();
}
}
return null;
return repository.findAll(Example.of(examplePlugin));
}
@Override

View File

@ -13,17 +13,17 @@ import org.springframework.boot.test.context.SpringBootTest;
@RunWith(Suite.class)
@SpringBootTest
@Suite.SuiteClasses({
OrganizationServiceTest.class,
ApplicationServiceTest.class,
LayoutServiceTest.class,
UserServiceTest.class,
PageServiceTest.class,
OrganizationServiceTest.class,
ApplicationServiceTest.class,
LayoutServiceTest.class,
UserServiceTest.class,
PageServiceTest.class,
})
public class ServerApplicationTests {
@Test
public void contextLoads() {
assert(true);
}
@Test
public void contextLoads() {
assert (true);
}
}

View File

@ -49,8 +49,8 @@ public class SeedMongoData {
{"validPageName"}
};
Object[][] pluginData = {
{"Installed Plugin Name", PluginType.REST, "installed-plugin"},
{"Not Installed Plugin Name", PluginType.REST, "not-installed-plugin"}
{"Installed Plugin Name", PluginType.API, "installed-plugin"},
{"Not Installed Plugin Name", PluginType.API, "not-installed-plugin"}
};
return args -> {
organizationRepository.deleteAll()

View File

@ -121,5 +121,5 @@ public class ApplicationServiceTest {
})
.verifyComplete();
}
}