Apifi

Java 8+ annotation processor for auto generation and rapid development of GraphQL APIs

License

License

GroupId

GroupId

org.sindaryn
ArtifactId

ArtifactId

apify
Last Version

Last Version

0.0.1
Release Date

Release Date

Type

Type

jar
Description

Description

Apifi
Java 8+ annotation processor for auto generation and rapid development of GraphQL APIs
Project URL

Project URL

https://github.com/sindaryn/apifi
Source Code Management

Source Code Management

http://github.com/sindaryn/apifi

Download apify

How to add to project

<!-- https://jarcasting.com/artifacts/org.sindaryn/apify/ -->
<dependency>
    <groupId>org.sindaryn</groupId>
    <artifactId>apify</artifactId>
    <version>0.0.1</version>
</dependency>
// https://jarcasting.com/artifacts/org.sindaryn/apify/
implementation 'org.sindaryn:apify:0.0.1'
// https://jarcasting.com/artifacts/org.sindaryn/apify/
implementation ("org.sindaryn:apify:0.0.1")
'org.sindaryn:apify:jar:0.0.1'
<dependency org="org.sindaryn" name="apify" rev="0.0.1">
  <artifact name="apify" type="jar" />
</dependency>
@Grapes(
@Grab(group='org.sindaryn', module='apify', version='0.0.1')
)
libraryDependencies += "org.sindaryn" % "apify" % "0.0.1"
[org.sindaryn/apify "0.0.1"]

Dependencies

compile (7)

Group / Artifact Type Version
com.github.sindaryn » datafi jar de10384
org.springframework.boot : spring-boot-starter-web jar 2.1.6.RELEASE
org.springframework.security : spring-security-web jar 5.1.6.RELEASE
io.leangen.graphql : graphql-spqr-spring-boot-starter jar 0.0.4
com.google.guava : guava jar 28.0-jre
com.squareup : javapoet jar 1.11.1
org.reflections : reflections jar 0.9.11

provided (2)

Group / Artifact Type Version
org.projectlombok : lombok jar 1.18.8
com.google.auto.service : auto-service jar 1.0-rc6

Project Modules

There are no modules declared in this project.

Apifi

Introduction

The Datafi library fully automates the generation and use of data access layer code, abstracting away the need for directly dealing with any JpaRepository. Apifi takes it to the next level by abstracting away the need to code the service or controller layers. The only thing required for the automatic generation of a full-fledged API, is a correctly annotated data model. This guide assumes the reader has a basic understanding of GraphQL schemas and APIs.

Installation

Apifi is available on maven central:

<dependency>
  <groupId>org.sindaryn</groupId>
    <artifactId>apify</artifactId>
  <version>0.0.1</version>
</dependency>

Requirements

  1. The main class must be annotated either with @SpringBootApplication, or @MainClass.
  2. All entities must have a public getId() method.

Hello World

Apifi autogenerates GraphQL Service beans, containing all requisite queries and resolvers for data model entities annotated with @GraphQLApiEntity.

Domain model

@Entity
@GraphQLApiEntity
public class Person{
    @Id
    private String id = UUID.randomUUID().toString();
    private String name;
    private Integer age;
    // getters & setters, etc...
}

Service layer

After compiling the project and taking a peek in the "target" folder, the following is the autogenerated GrappQL service:

@Service
@Transactional
@GraphQLApi
public class PersonGraphQLService {

  /* ... various injected dependencies ... */ 
  
  @GraphQLQuery(name = "allPersons")
  public List<Person> allPersons(int limit, int offset) {
    return ApiLogic.getAll(Person.class, personDataManager, limit, offset);
  }

  @GraphQLQuery(name = "getPersonById")
  public Person getPersonById(String input) {
    return ApiLogic.getById(Person.class, personDataManager,input);
  }

  @GraphQLQuery(name = "getPersonsById")
  public List<Person> getPersonsById(List<String> input) {
    return ApiLogic.getCollectionById(Person.class, personDataManager, input);
  }

  @GraphQLMutation(name = "addPerson")
  public Person addPerson(Person input) {
    Person entity = ApiLogic.add(personDataManager, input, personMetaOperations);
    return entity;
  }

  @GraphQLMutation(name = "updatePerson")
  public Person updatePerson(Person input) {
    Person entity = ApiLogic.update(personDataManager, input, reflectionCache, personMetaOperations);
    return entity;
  }

  @GraphQLMutation(name = "deletePerson")
  public Person deletePerson(Person input) {
    Person entity = ApiLogic.delete(personDataManager, reflectionCache, input, personMetaOperations);
    return entity;
  }

  @GraphQLMutation(name = "addPersons")
  public List<Person> addPersons(List<Person> input) {
    List<Person> entities = ApiLogic.addCollection(personDataManager, input, personMetaOperations);
    return entities;
  }

  @GraphQLMutation(name = "updatePersons")
  public List<Person> updatePersons(List<Person> input) {
    List<Person> entities = ApiLogic.updateCollection(personDataManager, input, personMetaOperations);
    return entities;
  }

  @GraphQLMutation(name = "deletePersons")
  public List<Person> deletePersons(List<Person> input) {
    List<Person> entities = ApiLogic.deleteCollection(personDataManager, input, personMetaOperations);
    return entities;
  }
}

The Service bean is named by appending the String "GraphQLService" to the pascal case name of the given entity - hence "PersonGraphQLService". As you can see, there are three annotations on the class:

  • @Service: Tells Spring to include it in the application context.
  • @Transactional: Tells Jpa that database operations happen within retractable transactions.
  • @GraphQLApi: Tells the SPQR Java-GraphQL framework (upon which this project heavily relies), to expose the contained queries and resolvers at the /graphql endpoint.

The standard CRUD schema

As in the above example, the standard, baseline graphql schema which is autogenerated contains the following GraphQL resolvers:

  1. all...

    @GraphQLQuery(name = "allPersons")
      public List<Person> allPersons(int limit, int offset) {
        return ApiLogic.getAll(Person.class, personDataManager, limit, offset);
      }
    

    Input: The paging limit and offset which tell the API how many results per page to return. Output: (limit - offset) entity instances.

  2. get...ById

      @GraphQLQuery(name = "getPersonById")
      public Person getPersonById(String input) {
        return ApiLogic.getById(Person.class, personDataManager,input);
      }
    

    Input: The id value by which to find the required entity instance. Output: The required entity instance.

  3. get...sById

      @GraphQLQuery(name = "getPersonsById")
      public List<Person> getPersonsById(List<String> input) {
        return ApiLogic.getCollectionById(Person.class, personDataManager, input);
      }
    

    Input: The id values by which to find the required entity instances. Output: The required entity instances.

  4. add...

      @GraphQLMutation(name = "addPerson")
      public Person addPerson(Person input) {
        Person entity = ApiLogic.add(personDataManager, input, personMetaOperations);
        return entity;
      }
    

    Input: An entity to add to the database. Output: The newly added entity.

  5. update...

      @GraphQLMutation(name = "updatePerson")
      public Person updatePerson(Person input) {
        Person entity = ApiLogic.update(personDataManager, input, reflectionCache, personMetaOperations);
        return entity;
      }
    

    Input: An entity "instance" with updated values with which to assign / overwrite the corresponding values of the entity with the same id. Output: The newly updated entity.

  6. delete...

      @GraphQLMutation(name = "deletePerson")
      public Person deletePerson(Person input) {
        Person entity = ApiLogic.delete(personDataManager, reflectionCache, input, personMetaOperations);
        return entity;
      }
    

    Input: An entity instance containing a valid id. Output: The newly deleted entity.

  7. add...s

        @GraphQLMutation(name = "addPersons")
        public List<Person> addPersons(List<Person> input) {
        List<Person> entities = ApiLogic.addCollection(personDataManager, input, personMetaOperations);
        return entities;
      }
    

    Input: A list of entities to add to the database. Output: The newly added entities.

  8. update...s

      @GraphQLMutation(name = "updatePersons")
      public List<Person> updatePersons(List<Person> input) {
        List<Person> entities = ApiLogic.updateCollection(personDataManager, input, personMetaOperations);
        return entities;
      }
    

    Input: A list of entities to update. Output: The newly updated entities.

  9. delete...s

      @GraphQLMutation(name = "deletePersons")
      public List<Person> deletePersons(List<Person> input) {
        List<Person> entities = ApiLogic.deleteCollection(personDataManager, input, personMetaOperations);
        return entities;
      }
    

    Input: A list of entities to delete. Output: The newly deleted entities.

@GraphQLApiEntity

This annotation tells marks an entity for inclusion within the autogenerated API schema. It accepts three arguments:

  1. boolean exposeDirectly() default true;: Tells Apifi whether this is an entity which should have it's own top-level queries and mutations exposed as part of the api schema. This is set to true by default, but it should be noted that in certain instances this should be set to false. For example, assume say we have two entities; Car and SteeringWheel. It's fairly obvious that in this case the composite entity called SteeringWheel has no independent lifecycle and is instantiable only within the context of Car. In this case, Car should be exposed directly as part of the schema, as opposed to SteeringWheel, which should be accessable and mutatable only by way of the queries and resolvers exposed by the Car graphql service.
  2. boolean readOnly() default false;: Assuming the previous value is set to true, this flag determines whether both the queries and mutations are to be included in the schema, or just the queries. This is useful when designating certain components of a given data model as read-only, with no possibility of state mutation via the API.
  3. Class<? extends ApiMetaOperations> apiMetaOperations() default ApiMetaOperations.class;: Often times, additional logical operations are required before and / or after the execution of a mutation. For example: Given a SaaS application onboarding system, which onboards Tenant entities. Assuming each tenant (e.g. company) requires its own unique subdomain, the relevant DNS registrars API should be called immediately following the initial onboarding. ApiMetaOperations<T> is the abstract base class which can be extended in order to achieve this. We'll get into it in more depth a bit further on.

Custom resolvers

In addition to the standard CRUD operation resolvers, custom resolvers can be added by making use of the following annotations:

  1. @GetBy: To fetch a list of entity instances with the search criteria being the value of a specific field, the field can be annotated with the @GetBy annotation and a custom List<...> get...By...(field value as arg) resolver will be added to the GraphQL service bean. For example:

    @Entity
    @GraphQLApiEntity
    public class Person {
        @Id
        private String id = UUID.randomUUID().toString();
        private String name;
        @GetBy
        private Integer age;
        private String address;
    }
    

    The above @GetBy would generate the following resolver:

    @...
    PersonGraphQLService{
    ...
      //fetch a list of people who are "age" years old
      @GraphQLQuery(name = "getPersonsByAge")
      public List<Person> getPersonsByAge(Integer age) {
        return ApiLogic.getBy(Person.class, personDataManager, "age", age);
      }
    ...
    }
    
  2. @GetAllBy: To fetch a list of entity instances with the search criteria being the value of a specific field being contained within a list of corresponding inputted values, the field can be annotated with the @GetAllBy annotation and a custom List<...> getAll...By...(list of field values to look for) resolver will be added to the GraphQL service bean. For example:

    @Entity
    @GraphQLApiEntity
    public class Person {
        @Id
        private String id = UUID.randomUUID().toString();
        @GetAllBy
        private String name;
        private Integer age;
        private String address;
    }
    

    The above @GetAllBy would generate the following resolver:

    @...
    PersonGraphQLService{
    ...
      //fetch a list of people whos name is included in the "names" list
      @GraphQLQuery(name = "getAllPersonsByNames")
      public List<Person> getAllPersonsByNames(List<String> names) {
        return ApiLogic.getAllBy(Person.class, personDataManager, "name", names);
      }
    ...
    }
    
  3. @GetByUnique: To fetch a single entity instance by the value of a unique field, the field can be annotated with the @GetByUnique annotation and a custom get...ByUnique...(field value) resolver will be added to the GraphQL service bean. For example:

    @Entity
    @GraphQLApiEntity
    public class Person {
        @Id
        private String id = UUID.randomUUID().toString();
        private String name;
        private Integer age;
        private String address;
        @Column(unique = true) @GetByUnique
        private String socialSecurityNumber;
    }
    

    The above @GetByUnique would generate the following resolver:

    @...
    PersonGraphQLService{
      ...
      @GraphQLQuery(name = "getPersonBySocialSecurityNumber")
      public Person getPersonBySocialSecurityNumber(String socialSecurityNumber) {
        return ApiLogic.getByUnique(Person.class, personDataManager, "socialSecurityNumber", socialSecurityNumber);
      }
    ...
    }
    
  4. @WithResolver(...): To fetch a list of entity instances using a custom JPQL query, the relevant entity-class can be annotated with the @WithResolver(...) annotation and a corresponding resolver will be added to the GraphQL service bean. The @WithResolver(...) annotation feature is a bit more involved than the others, and builds upon it's implementation in the Datafi library. If you are unfamiliar with it's usage, head on over to the Datafi Readme. As is the case with the previous three annotations, Apifi extends the functionality provided by Datafi by generating the code required to expose that data layer resolver to the web.

    In any event, here is an example of @WithResolver usage:

    @Entity
    @GraphQLApiEntity
    @WithResolver(name = "getPersonsByNameOrAge", where = "p.name = :name OR p.age = :age", args = {"name", "age"})
    public class Person {
        @Id
        private String id = UUID.randomUUID().toString();
        private String name;
        private Integer age;
        private String address;
    }
    

    The above @WithResolver(...) would generate the following resolver:

    @...
    PersonGraphQLService{
    ...
      @GraphQLQuery(name = "getPersonsByNameOrAge")
      public List<Person> getPersonsByNameOrAge(String name, Integer age) {
        List<Object> args = Arrays.asList(name, age);
        return ApiLogic.selectBy(Person.class, personDataManager, "getPersonsByNameOrAge", args);
      }
    ...
    }
    

    Important note: @WithResolver(...) is a repeatable annotation, therefore as many custom JPQL query based resolvers per class may be assigned as needed.

Collections

Iterable<...> collections are unique in that they are not "assigned" per say - they're added to, updated in, and removed from. As such, some specialized resolvers are required in order to work with them. Before we can demonstrate these, we'll need to add a collection to our example. Fortunately, our Person appears to have picked up a few hobbies!

@Entity
@GraphQLApiEntity
public class Person {
    @Id
    private String id = UUID.randomUUID().toString();
    private String name;
    private Integer age;
    private String address;
    @OneToMany
    private Set<Hobby> hobbies;
}

And our new Hobby entity:

@Entity
@GraphQLApiEntity(exposeDirectly = false)
public class Hobby {
    @Id
    private String id = UUID.randomUUID().toString();
    private String title;
    private String description;
}

Let's have a look at the generated code, method by method:

  1. Read collection:

    @GraphQLQuery(name = "hobbies")
    public List<List<Hobby>> hobbies(@GraphQLContext List<Person> input) {
    return ApiLogic.getAsEmbeddedCollection(...);
    }
    

    This query resolver allows graphql queries such as the following to be executed:

    query getHobbiesOfPersons{
      allPersons(limit:25, offset:0){
      ...
        hobbies{
          id
          title
          description
        }
      ...
      }
    }
    

    This should look familiar to anyone with a working knowledge of GraphQL. It's the typical way you'd expect to retreive a foreign key collection.

  2. Add / attach to collection:

    @GraphQLMutation(name = "addNewHobbiesToPerson")
    public List<Hobby> addNewHobbiesToPerson(List<Hobby> input, Person person) {
        return ApiLogic.addNewToEmbeddedCollection(...);
    }
    

    This mutation accepts two arguments:

    • List<...> input: The entities - in this case; hobbies, to add the the given person. Recall the first argument to @GraphQLApiEntity is boolean exposeDirectly(). If the type of entity referenced in the given collection is marked as one to be exposedDirectly(), it makes no sense to create new instances within the context of an embedded collection. However in this instance hobby is not marked for direct API exposure, hence the addNewHobbiesToPerson resolver, and not the attachExisting...To... equivalent resolver for opposite cases.
    • Person person: The host entity which the collection in question belongs to.
  3. Update elements in collection:

      @GraphQLMutation(name = "updateHobbiesInPerson")
      public List<Hobby> updateHobbiesInPerson(List<Hobby> input, Person person) {
        return ApiLogic.updateEmbeddedCollection(...);
      }
    

    Accepts two arguments following a similar concept as the previous mutation - a list of elements to be updated, and a reference to the host entity. The method makes use of application-level "cascading", therefore obviating the need to worry about database-level cascading on update operations.

  4. Remove elements from collection:

      @GraphQLMutation(name = "removeHobbiesFromPerson")
      public List<Hobby> removeHobbiesFromPerson(List<Hobby> input, Person person) {
        return ApiLogic.removeFromEmbeddedCollection(...);
      }
    

    Accepts two arguments following a similar concept as the previous two mutations - a list of elements to be removed, and a reference to the host entity. Note that this method does NOT actually perform any deletions, but rather merely removes the foreign key references.

ApiMetaOperations

As briefly mentioned above, Apifi APIs are designed for extensibility. This is where ApiMetaOperations<T> comes in. To get a better understanding as to how this works, let's take a peek into the ApiLogic interface where we keep our actual api-level logic - or more the specifically the addCollection method:

    static <T, E extends ApiMetaOperations<T>> List<T>
    addCollection(DataManager<T> dataManager, List<T> input, E metaOps) {
        if (metaOps != null) metaOps.preAddEntities(input);
        val result = dataManager.saveAll(input);
        if (metaOps != null) metaOps.postAddEntities(result);
        return result;
    }

Note the two lines if (metaOps != null) metaOps.preAddEntities(input);, and if (metaOps != null) metaOps.postAddEntities(result);. These method calls allow for the metaOps instance which was passed as an argument to execute custom defined logic before and / or after the state mutation. This pattern repeats itself in the same manner in all of the other ApiLogic methods, allowing for the execution of custom defined logic prior to or following state mutations.

In order to make use of this, the class type token of the developers custom child-class of ApiMetaOperations<T> must be passed as the third argument to the @GraphQLApiEntity annotation. In order to understand how to make practical use of this feature, observe the ApiMetaOperations<T> source code:

@Component
public class ApiMetaOperations<T> {

    @Autowired @Getter
    private ReflectionCache reflectionCache;
    @Autowired @Getter
    private DataManager<T> dataManager;

    public void preAddEntity(T toAdd){}
    public void postAddEntity(T added){}
    public void preUpdateEntity(T toUpdate){}
    public void postUpdateEntity(T toUpdate){}
    public void preDeleteEntity(T toDelete){}
    public void postDeleteEntity(T deleted){}

    public void preAddEntities(Collection<T> toAdd){toAdd.forEach(this::preAddEntity);}
    public void postAddEntities(Collection<T> added){added.forEach(this::postAddEntity);}
    public void preUpdateEntities(Collection<T> toUpdate){toUpdate.forEach(this::preUpdateEntity);}
    public void postUpdateEntities(Collection<T> toUpdate){toUpdate.forEach(this::postUpdateEntity);}
    public void preDeleteEntities(Collection<T> toDelete){toDelete.forEach(this::preDeleteEntity);}
    public void postDeleteEntities(Collection<T> deleted){deleted.forEach(this::postDeleteEntity);}
}

The methods and code are fairly self-explanatory. As is observable, the default ApiMetaOperations<T> class which is passed as that third argument to GraphQLApiEntity has no actual impact. To override this default behaviour (or lack thereof), the base class must be extended and implemented as required. Let's go through a familiar example. Given our Person entity:

@Entity
@GraphQLApiEntity
public class Person{
    @Id
    private String id = UUID.randomUUID().toString();
    private String name;
    private Integer age;
    // getters & setters, etc...
}

Assume there is a requirement to print a welcome message to the console after every time a new person is added, and a goodbye message after every time a person is deleted. To do so, a custom implementation of ApiMetaOperations<T> can be implemented as follows:

@Component
public class PersonMetaOperations extends ApiMetaOperations<Person> {

    @Override
    public void postAddEntity(Person added) {
        System.out.println("Hello " + added.getName() + "!");
    }

    @Override
    public void postDeleteEntity(Person deleted) {
        System.out.println("Goodbye " + deleted.getName() + ":(");
    }
    
}

The final step is to pass the corresponding class type token as an argument to the @GraphQLApiEntity annotation on Person:

@Entity
@GraphQLApiEntity(apiMetaOperations = PersonMetaOperations.class)
public class Person{
    @Id
    private String id = UUID.randomUUID().toString();
    private String name;
    private Integer age;
    // getters & setters, etc...
}

The required custom behaviour is now fully defined and operational. Obviously this is a trivial example, but the same workflow applies to more complex use cases such as third party API calls, data related metrics, etc.

EmbeddedCollectionMetaOperations

As demostrated above, embedded collections must be dealt with in their own way. This is why the standard ApiMetaOperations<T> cannot apply to mutations performed on embedded collections. In order to add custom pre or post mutation logic to embedded collection related api mutations, the collection in question must first be annotated with the @MetaOperations(...) annotation, as follows:

@Entity
@GraphQLApiEntity(apiMetaOperations = PersonMetaOperations.class)
public class Person{
    @Id
    private String id = UUID.randomUUID().toString();
    private String name;
    private Integer age;
    @ManyToMany
    @MetaOperations(...)
    private Set<Person> friends;
}

Next, the EmbeddedCollectionMetaOperations<T, HasTs> base class must be extended. The base EmbeddedCollectionMetaOperations<T, HasTs> class looks as follows:

@Component
public class EmbeddedCollectionMetaOperations<T, HasTs>{
    
    @Autowired @Getter
    private ReflectionCache reflectionCache;
    @Autowired @Getter
    private DataManager<T> tDataManager;
    @Autowired @Getter
    private DataManager<HasTs> hasTsDataManager;

    public void preRemove(Collection<T> Ts, HasTs hasTs){}
    public void postRemove(Collection<T> Ts, HasTs hasTs){}
    public void preAttachOrAdd(Collection<T> Ts, HasTs hasTs){}
    public void postAttachOrAdd(Collection<T> Ts, HasTs hasTs){}
    public void preUpdate(Collection<T> Ts, HasTs hasTs){}
    public void postUpdate(Collection<T> Ts, HasTs hasTs){}
}

The code above should also be fairly self explanatory. To illustrate, let's implement a child class which prints the name of each newly added hobby:

@Component
public class HobbiesInPersonMetaOperations extends EmbeddedCollectionMetaOperations<Hobby, Person> {
    
    @Override
    public void preAttachOrAdd(Collection<Hobby> hobbies, Person person) {
        hobbies.forEach(newHobby -> System.out.println(newHobby.getDescription()));
    }
    
}

Finally, the class type token for HobbiesInPersonMetaOperations must be passed as an argument to the @MetaOperation(...) annotation on the Set<Hobby> hobbies collection within Person:

@Entity
@GraphQLApiEntity(apiMetaOperations = PersonMetaOperations.class)
public class Person{
    @Id
    private String id = UUID.randomUUID().toString();
    private String name;
    private Integer age;
    @ManyToMany
    @MetaOperations(metaOps = HobbiesInPersonMetaOperations.class)
    private Set<Hobby> hobbies;
}

Spring security integration

Apifi supports full integration with spring security - or more specifically; spring security annotations. This is feature can be utilized via the@Secure(...) annotation. This annotation can be applied on the class or field level.

Overview

Here's how the @Secure annotation looks on the inside:

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Secure {
    String secured() default "";
    String rolesAllowed() default "";
    String preAuthorize() default "";
    String postAuthorize() default "";
    String preFilter() default "";
    String preFilterTarget() default "";
    String postFilter() default "";

    String securedCreate() default "";
    String rolesAllowedCreate() default "";
    String preAuthorizeCreate() default "";
    String postAuthorizeCreate() default "";
    String preFilterCreate() default "";
    String preFilterTargetCreate() default "";
    String postFilterCreate() default "";

    String securedRead() default "";
    String rolesAllowedRead() default "";
    String preAuthorizeRead() default "";
    String postAuthorizeRead() default "";
    String preFilterRead() default "";
    String preFilterTargetRead() default "";
    String postFilterRead() default "";

    String securedUpdate() default "";
    String rolesAllowedUpdate() default "";
    String preAuthorizeUpdate() default "";
    String postAuthorizeUpdate() default "";
    String preFilterUpdate() default "";
    String preFilterTargetUpdate() default "";
    String postFilterUpdate() default "";

    String securedDelete() default "";
    String rolesAllowedDelete() default "";
    String preAuthorizeDelete() default "";
    String postAuthorizeDelete() default "";
    String preFilterDelete() default "";
    String preFilterTargetDelete() default "";
    String postFilterDelete() default "";
}

@Secure enables the application of any or all of 6 spring & javax security annotations: @Secured, @RolesAllowed, @PreAuthorize, @PostAuthorize, @PreFilter, and @PostFilter.

Let's get specific with an example:

@Entity
@GraphQLApiEntity
@Secure(rolesAllowed = "ROLE_ADMIN")
public class Person {
    @Id
    private String id = UUID.randomUUID().toString();
    private String name;
    private Integer age;
    private String address;
}

Here's the resulting service bean:

@...
@RolesAllowed("ROLE_ADMIN")
public class PersonGraphQLService {...}

Note the @RolesAllowed("ROLE_ADMIN") annotation. We now have spring security ensuring that only users which spring security recognizes as having "ROLE_ADMIN" will be able to access and execute any of the contained methods.

Importnant note: This does not preconfigure spring security itself, but rather merely applies spring security annotations. The final security setup is up to the developer.

Oftentimes, more granular control over which methods can be accessed and executed by which users is required, and just slapping on a class level security annotation won't cut it. The automated application of spring security annotations to classes and methods falls into one of two categories:

  1. Annotation parameters that were passed as arguments to the class level @Secure(...) annotation (as in the above example). These annotations fall into one of 5 sub-categories:

    • CRUD:

      @...
      public @interface Secure {
          String secured() default "";
          String rolesAllowed() default "";
          String preAuthorize() default "";
          String postAuthorize() default "";
          String preFilter() default "";
          String preFilterTarget() default "";
          String postFilter() default "";
          ...
      }
      

      Note these top-most annotation arguments are named after their corresponding security annotations with no suffix. They correspond to the annotations that are placed at the class level - as with our above example.

    • Create:

       @...
       public @interface Secure {
           ...
           String securedCreate() default "";
           String rolesAllowedCreate() default "";
           String preAuthorizeCreate() default "";
           String postAuthorizeCreate() default "";
           String preFilterCreate() default "";
           String preFilterTargetCreate() default "";
           String postFilterCreate() default "";
           ...
       }
      

      As is the case with all of the following sub-categories as well, these annotations are for application at the method level. More specifically, they are applicable to create methods - i.e. add... and add...s.

    • Read:

      @...
      public @interface Secure {
          ...
          String securedRead() default "";
          String rolesAllowedRead() default "";
          String preAuthorizeRead() default "";
          String postAuthorizeRead() default "";
          String preFilterRead() default "";
          String preFilterTargetRead() default "";
          String postFilterRead() default "";
          ...
      }
      

      As their suffixes suggest, these are applicable to read - i.e get... operations.

    • Update:

      @...
      public @interface Secure {
         ...
         String securedUpdate() default "";
         String rolesAllowedUpdate() default "";
         String preAuthorizeUpdate() default "";
         String postAuthorizeUpdate() default "";
         String preFilterUpdate() default "";
         String preFilterTargetUpdate() default "";
         String postFilterUpdate() default "";
         ...
      }
      

      As their suffixes suggest, these are applicable to update... operations.

    • Delete:

      @...
      public @interface Secure {
         ...
         String securedDelete() default "";
         String rolesAllowedDelete() default "";
         String preAuthorizeDelete() default "";
         String postAuthorizeDelete() default "";
         String preFilterDelete() default "";
         String preFilterTargetDelete() default "";
         String postFilterDelete() default "";
         ...
      }
      

      As their suffixes suggest, these are applicable to delete... operations.

    Let's showcase these subcategories with the following example:

    @Entity
    @GraphQLApiEntity
    @Secure(
        rolesAllowed = "permitAll()", 
        rolesAllowedCreate = "ROLE_ADMIN", 
        rolesAllowedUpdate = "ROLE_SECRETARY",
        rolesAllowedDelete = "ROLE_SUPERVISOR"
        )
    public class Person {
        @Id
        private String id = UUID.randomUUID().toString();
        private String name;
        private Integer age;
        private String address;
    }
    

    And the resulting service bean:

    @...
    @RolesAllowed("permitAll()")
    public class PersonGraphQLService {
      
      ...
      
      @GraphQLQuery(name = "allPersons")
      public List<Person> allPersons(int limit, int offset) {
        return ApiLogic.getAll(Person.class, personDataManager, limit, offset);
      }
    
      @GraphQLQuery(name = "getPersonById")
      public Person getPersonById(String input) {
        return ApiLogic.getById(Person.class, personDataManager,input);
      }
    
      @GraphQLQuery(name = "getPersonsById")
      public List<Person> getPersonsById(List<String> input) {
        return ApiLogic.getCollectionById(Person.class, personDataManager, input);
      }
    
      @GraphQLMutation(name = "addPerson")
      @RolesAllowed("ROLE_ADMIN")
      public Person addPerson(Person input) {
        Person entity = ApiLogic.add(personDataManager, input, personMetaOperations);
        return entity;
      }
    
      @GraphQLMutation(name = "updatePerson")
      @RolesAllowed("ROLE_SECRETARY")
      public Person updatePerson(Person input) {
        Person entity = ApiLogic.update(personDataManager, input, reflectionCache, personMetaOperations);
        return entity;
      }
    
      @GraphQLMutation(name = "deletePerson")
      @RolesAllowed("ROLE_SUPERVISOR")
      public Person deletePerson(Person input) {
        Person entity = ApiLogic.delete(personDataManager, reflectionCache, input, personMetaOperations);
        return entity;
      }
    
      @GraphQLMutation(name = "addPersons")
      @RolesAllowed("ROLE_ADMIN")
      public List<Person> addPersons(List<Person> input) {
        List<Person> entities = ApiLogic.addCollection(personDataManager, input, personMetaOperations);
        return entities;
      }
    
      @GraphQLMutation(name = "updatePersons")
      @RolesAllowed("ROLE_SECRETARY")
      public List<Person> updatePersons(List<Person> input) {
        List<Person> entities = ApiLogic.updateCollection(personDataManager, input, personMetaOperations);
        return entities;
      }
    
      @GraphQLMutation(name = "deletePersons")
      @RolesAllowed("ROLE_SUPERVISOR")
      public List<Person> deletePersons(List<Person> input) {
        List<Person> entities = ApiLogic.deleteCollection(personDataManager, input, personMetaOperations);
        return entities;
      }
    }
    

    The breakdown:

    • On the class level, spring security is set to "permitAll()", which is therefore the default for all contained methods which do not have their own overriding security annotation. In this particular case, this includes all read related resolvers such as all..., get...ById, and get...sById. Recall the rolesAllowed = "permitAll()" argument we passed to @Secure.

    • The create, or add... methods are annotated with @RolesAllowed("ROLE_ADMIN"), such that only users with an admin role can use them. Recall the rolesAllowedCreate = "ROLE_ADMIN" we passed to @Secure.

    • the update... methods are annotated with @RolesAllowed("ROLE_SECRETARY"). Recall the rolesAllowedUpdate = "ROLE_SECRETARY" argument that was passed to @Secure.

    • The delete... methods are annotated with @RolesAllowed("ROLE_SUPERVISOR"). Recall the rolesAllowedDelete = "ROLE_SUPERVISOR" argument passed to @Secure.

  2. Annotation arguments that were passed to a field level @Secure. Specifically, a field level @Secure(...) adorning a field also annotated with a @GetBy, @GetAllBy or @GetByUnique annotation. This is useful for defining granular access control for the corresponding API exposed resolvers. For example:

    @Entity
    @GraphQLApiEntity
    public class Person {
        @Id
        private String id = UUID.randomUUID().toString();
        @GetBy @Secure(preAuthorize = "ROLE_ADMIN")
        private String name;
        private Integer age;
        private String address;
    }
    

    Results in the following autogenerated code:

      @GraphQLQuery(name = "getPersonsByName")
      @PreAuthorize("ROLE_ADMIN")
      public List<Person> getPersonsByName(String name) {
        return ApiLogic.getBy(Person.class, personDataManager, "name", name);
      }
    

    The same flow applies to @GetAllBy and @GetByUnique.

That's all for now, happy coding!

License

Apache 2.0

Versions

Version
0.0.1