Customizing field arguments in Brightspot Content Delivery API
There exist use cases where the default id and path arguments exposed in a Content Delivery API (CDA) do not fit the intended functionality. Fortunately, they are flexible!
Prerequisites
For endpoint configuration: Custom Content Delivery API development or Hello Content Delivery API.
Introduction
When exposing GraphQL APIs via Brightspot’s view system, data is typically queried for by ID or path. However, there are cases in which callers may want to query by some other criteria, like a third-party ID or maybe even remove the arguments altogether, leaving object resolution up to the view model.
Background
When it comes to removing the arguments completely, we can accomplish this by using the correct generic argument for the relevant ViewModel class. There are two ways to go about this:
- Leverage Brightspot’s Singleton interface on your view model’s model.
- Utilize your ContentDeliveryApiEndpoint subclass as your view model’s model.
For adding custom arguments we can leverage Brightspot’s @WebParameter annotation. It allows us to treat scalar fields, such as integers, strings, and even lists of them, as an argument.Additionally, we can create custom Java annotations that are utilized to denote a field that should be transformed into an argument with a custom input type, and even populate a Java POJO, by using ContentDeliveryApiWebAnnotationProcessor.
Examples
View model with singleton model
public class Homepage extends Content implements Singleton {
private String title;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
@ViewInterface
public class HomePageViewModel extends ViewModel<Homepage> {
public String getTitle() {
return model.getTitle();
}
}
The following types are generated in the resulting schema:
type Query {
Homepage: Homepage
}
type Homepage {
title: String
}
Callers are now able to query for the homepage without any arguments since the system knows how to find the one and only.
API entry field
Sometimes API designers may not need to use a data model, or may want to implement their own logic to resolve the proper data model. In this case, they can just use their endpoint class as the model for the view model.
Also, as discussed earlier, they can leverage @WebParameter to expose their own scalar arguments.
Consider the following classes:
public class Article extends Content {
private String headline;
public String getHeadline() {
return headline;
}
public void setHeadline(String headline) {
this.headline = headline;
}
}
@ViewInterface
public class ArticleViewModel extends ViewModel<Article> {
public String getHeadline() {
return model.getHeadline();
}
}
@ViewInterface
public class ArticleSearchViewModel extends ViewModel<ArticleEndpoint> {
@WebParameter
private String search;
@WebParameter
private Integer limit = 5;
public Iterable<ArticleViewModel> getArticles() {
Query<Article> articleQuery = Query.from(Article.class);
if (!StringUtils.isBlank(search)) {
articleQuery.where("* matches ?", search);
}
final int maxLimit = 50;
return createViews(ArticleViewModel.class, articleQuery
.select(0, Math.min(limit, maxLimit)).getItems());
}
}
public class ArticleEndpoint extends ContentDeliveryApiEndpoint {
@Override
protected String getPathSuffix() {
return "/article";
}
@Override
public List<ContentDeliveryEntryPointField> getQueryEntryFields() {
return Collections.singletonList(new ContentDeliveryEntryPointField(ArticleSearchViewModel.class));
}
}
The following types are generated in the resulting schema:
type Query {
ArticleSearch(search: String, limit: Int): ArticleSearch
}
type ArticleSearch {
articles: [Article]
}
type Article {
headline: String
}
The @WebParameter annotated fields are assigned to their respective argument value passed in the GraphQL query, and are utilized in the view model logic. Callers are now able to query for articles via a general full-text search.
Custom input type
API designers may want to expose a structured input type and have it populate some custom type on the back end. This is easily achievable with a custom Java annotation and a ContentDeliveryApiWebAnntoationProcessor implementation.
Consider the following classes:
public class Person {
private String name;
private String location;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface CurrentPerson {
}
public class CurrentPersonProcessor implements ContentDeliveryApiWebAnnotationProcessor<CurrentPerson> {
@Override
public Object getValue(ApiRequest request, Object input, Field field, CurrentPerson annotation) {
String name = (String) CollectionUtils.getByPath(input, "name");
String location = (String) CollectionUtils.getByPath(input, "location");
Person person = new Person();
person.setName(name);
person.setLocation(location);
return person;
}
@Override
public SchemaInputType getInputType(Field field, CurrentPerson annotation) {
return SchemaInputTypes.newInputObject("Person")
.field("name", SchemaInputTypes.nonNullOf(SchemaInputTypes.STRING))
.field("location", SchemaInputTypes.nonNullOf(SchemaInputTypes.STRING))
.build();
}
}
public class GreetingEndpoint extends ContentDeliveryApiEndpoint {
@Override
protected String getPathSuffix() {
return "/greeting";
}
@Override
public List<ContentDeliveryEntryPointField> getQueryEntryFields() {
return Collections.singletonList(new ContentDeliveryEntryPointField(GreetingViewModel.class));
}
}
@ViewInterface
public class GreetingViewModel extends ViewModel<GreetingEndpoint> {
@CurrentPerson
private Person person;
public String getGreeting() {
return "Hello " + person.getName() + " from " + person.getLocation() + "!";
}
}
The following types are generated in the resulting schema:
type Query {
Greeting(person: Person): Greeting
}
type Greeting {
greeting: String
}
input Person {
name: String!
location: String!
}
The @CurrentPerson annotated field is assigned to its respective argument value passed in the GraphQL query, and is utilized in the view model logic.