Inspired by built-in fake REST data provider react-admin with queries like:
GET /movies?filter={id: 1} //get movies by id = 1
GET /movies?filter={id: [1,2]} //get movies by id = 1 or id = 2
GET /actors?filter={movies: 1, firstName: John} //actors played in movie with id = 1 and their first name is John
GET /actors?filter={birthDateGt: 1960}&sort=[id,DESC]&range=[0,100] //actors born later than 1960
GET /actors?filter={q: %Keanu Re%} //full text search on all text fields
GET /actors?sort=[firstName,DESC,birthDate,ASC] //sort by multiple fields in case of ties
More Inspiration was drawn from languages like FIQL/RSQL so recently more features were added along with in-memory integration tests, support for non-number primary keys, resulting in a total refactoring of the code and fix of a lot of bugs (there are still some edge cases).
Now it is possible to also do the following (after url-encode of the query part of the url):
GET /movies?filter={idNot: 1} //get movies with id not equal to 1
GET /actors?filter={movies: null} //actors that have played in no movie
GET /actors?filter={moviesNot: null} //actors that have played to a movie
GET /actors?filter={movies: [1,2]} //actors played in either movie with id = 1, or movie with id = 2
GET /actors?filter={moviesAnd: [1,2]} //actors played in both movies with id = 1 and id = 2
GET /actors?filter={moviesNot: [1,2]} //actors played in neither movie with id = 1, nor movie with id = 2
GET /actors?filter={name: Keanu Ree%} //full text search on specific fields just by the inclusion of one or two '%' in the value
GET /actors?filter={movies: {name: Matrix}} //actors that have played in movie with name Matrix
GET /actors?filter={movies: {name: Matrix%}} //actors that have played in movies with name starting with Matrix
GET /movies?filter={actors: {firstName: Keanu, lastNameNot: Reves}} //movies with actors that firstName is 'Keanu' but lastName is not 'Reves'
GET /actors?filter=[{firstName: Keanu},{firstName: John}] //actors with firstName 'Keanu' or 'John'
GET /actors?filter={firstName: [Keanu, John]} //equivalent to the above
GET /documents?filter={uuid: f44010c9-4d3c-45b2-bb6b-6cac8572bb78} //get document with java.util.UUID equal to f44010c9-4d3c-45b2-bb6b-6cac8572bb78
GET /libraries?filter={documents: {uuid: f44010c9-4d3c-45b2-bb6b-6cac8572bb78}} //get libraries that contain document with uuid equal to f44010c9-4d3c-45b2-bb6b-6cac8572bb78
GET /libraries?filter={documents: f44010c9-4d3c-45b2-bb6b-6cac8572bb78} //same as above
The key names are not the ones on the database but the ones exposed by the REST API and are the names of the entity attribute names. Here movies
is plural because an Actor has @ManyToMany
annotation on List<Movie> movies
attribute.
- Keep in mind that key/value pairs that are in { } are combined by default with AND.
/actors?filter={firstName:'A',lastName:'B'} => firstName = A and lastName = B
- Values or Objects that contain key/values in [] are combined by default with OR unless the key in front of the [] is ending with 'And'.
/actors?filter={movies: [1,2]} => actors having acted at movies with ids 1 OR 2
/movies?filter={actors: [{firstName:'A'}, {lastName:'B'}] } => movies having actors with firstName = A OR lastName = B
/actors?filter={moviesAnd: [1,2]} => actors acted at movies with ids 1 AND 2
/movies?filter={actorsAnd: [{firstName:'A'}, {lastName:'B'}] } => movies having actors with firstName = A AND lastName = B
The above functionality is possible via this simple setup:
@RestController
@RequestMapping("actors")
public class ActorController {
@Autowired
private ActorRepository repository;
@Autowired
private FilterService<Actor, Long> filterService;
@GetMapping
public Iterable<Actor> filterBy(
@RequestParam(required = false, name = "filter") String filterStr,
@RequestParam(required = false, name = "range") String rangeStr,
@RequestParam(required = false, name="sort") String sortStr) {
QueryParamWrapper wrapper = QueryParamExtractor.extract(filterStr, rangeStr, sortStr);
return filterService.filterBy(wrapper, repository, Arrays.asList("firstName", "lastName"));
}
}
The main important parts include:
@ControllerAdvices
that wrap Collections in objects {content: []) with paging and number of results information along with Status Codes based on Exceptions thrown and returns 404 in case of null returned from endpoints. Place here additional functionality annotated with @ControllerAdvice and extending the appropriate class depending on your needs.BaseRepository
interface that needs to be extended by each of resourceRepositories
CustomSpecifications
does all the magic of Criteria API query generation so that filtering and sorting works along withFilterService
that provides some helper methods to the Controller code and helps provide convert the String query params toFilterWrapper
so that it can be injected behind the scenes.ObjectMapperProvider
that can be used by the Spring Boot Application in case serialization and deserialization need to work through fields instead of Getters and Setters
For now installation is done through jitpack:
Add this in your pom.xml repositories:
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
...
</repositories>
and add this as a dependency in your pom.xml dependencies:
<dependency>
<groupId>com.github.zifnab87</groupId>
<artifactId>spring-boot-rest-api-helpers</artifactId>
<version>edb1770</version> <!-- or latest short commit id -->
</dependency>
- Add springboot.rest package in the scanBasePackages at the top of your Spring Boot Application class
@SpringBootApplication(scanBasePackages = {"com.myproject", "springboot.rest"})
- inject and expose as
@Bean
the providedObjectMapperProvider
@Autowired
private ObjectMapperProvider objMapperProvider;
@Bean
public ObjectMapper objectMapper() {
return objMapperProvider.getObjectMapper();
}
- configure appplication.properties to use snake-case or camelCase for properties in API
spring-boot-rest-api-helpers.use-snake-case = false
- for each of the Rest API resources create a class
XYZ
that is annotated with@Entity
- for each of the Rest API resources create an interface
XYZRepository
that extendsBaseRepository<XYZ,KeyType>
- for each of the Rest API resources create a class
XYZController
annotated with@RestController
for more examples see/run the integration tests with mvn -Dtest=filterByTests test
For an example of how it can be used along with admin-on-rest (a predecessor of React Admin), see admin-on-rest-demo-java-rest