spring-boot-rest-api-helpers

Inspired by built-in fake REST data provider react-admin (see documentation) that queries like that:

    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.

Important: Keep in mind that the object/array that is passed in filter needs to be url encoded for the request to work. E.g in Javascript someone would use encodeURIComponent() like that

let filterObj = {movies: [1,2]};
fetch('/actors?filter=' + encodeURIComponent(JSON.stringify(filterObj)));

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:

Installation

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>

Usage

    @Autowired
    private ObjectMapperProvider objMapperProvider;

    @Bean
    public ObjectMapper objectMapper() {
        return objMapperProvider.getObjectMapper();
    }

for more examples see/run the integration tests Note: three-level join tests are failing and are not implemented yet - Any help towards an implementation that allows any number of depth for queries would be greatly appreciated :D

Previous Versions

This repo used to be called react-admin-java-rest and it was used to provide the needed building blocks for building a real backend API like that can give responses to the above requests in conjuction with react-admin/admin-on-rest (used here together: https://github.com/zifnab87/admin-on-rest-demo-java-rest). Since the time of their first incarnation, it seemed obvious that those API helpers were useful outside of the react-admin REST API realm, so the name spring-boot-rest-api-helpers was given.

Fully working example (outdated)

For an example of how it can be used along admin-on-rest there is a fork of admin-on-rest-demo that is fully working and uses react-admin-java-rest

Fully Working Fork of admin-on-rest-demo: react-admin-demo-java-rest