64
loading...
This website collects cookies to deliver better user experience
todolist-api-with-faunadb
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── demo
│ │ └── todolistapiwithfaunadb
│ │ └── TodolistApiWithFaunadbApplication.java
│ └── resources
│ ├── application.properties
│ ├── static
│ └── templates
└── test
└── java
└── com
└── demo
└── todolistapiwithfaunadb
└── TodolistApiWithFaunadbApplicationTests.java
pom.xml
. Update your project ‘s pom.xml
file to have the dependency below present: // Fauna Maven dependency
<dependency>
<groupId>com.faunadb</groupId>
<artifactId>faunadb-java</artifactId>
<version>4.0.1</version>
<scope>compile</scope>
</dependency>
FaunaClient
in the Spring Boot project.FaunaClient
in Spring Boot Java, locate the Main application class TodolistApiWithFaunadbApplication.java
in src/main/java/com/demo/todolistapiwithfaunadb
path which annotates with @SpringBootApplication annotation.application.properties
file, add environment variable fauna-db.secret
to protect your secret. You will use a private
variable to store your key secret for configuring FaunaClient
in the Main application class.fauna-db.secret=fnAELIH5_EACBbjxHugMSRxZ0yZxemDWU2Q-ynkE
private
variable serverKey
:@Value("${fauna-db.secret}")
private String serverKey;
@Value
annotation injects the value of fauna-db.secret
from the application.properties
into the serverKey
variable field.FaunaClient
, create a bean using the @bean
method-level annotation. Next, make the bean of single scope using the @Scope
annotation. The @Scope
creates a single instance of the bean configuration; all requests for the bean will return the same object, which is cached.@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
public FaunaClient faunaConfiguration() {
FaunaClient faunaClient = FaunaClient.builder()
.withSecret(serverKey)
.build();
return faunaClient;
}
FaunaClient
connects to Fauna using the serverKey
provided.FaunaClient
anywhere in your Spring Boot project and be sure it queries to your cloud database.FaunaClient
, check out Fauna Java documentation.Todo
documents. To create the todos collection, run this query in the shell:CreateCollection(
{ name: "todos" }
);
{
ref: Collection("todos"),
ts: 1623105286880000,
history_days: 30,
name: "todos"
}
todos
collection:CreateIndex(
{
name: "all_todos",
source: Class("todos")
}
);
FaunaClient
in your Spring Boot project and learned how to write FQL queries via the shell on the Fauna dashboard. Next, you will implement the Todo List API with Fauna and test it.Endpoints | Functionality |
---|---|
POST /todos | Create a new todo |
GET /todos/{id} | Get a todo |
PUT /todos/{id} | Update a todo |
DELETE /todos/{id} | Delete a todo |
GET /todos | List all todos |
├── src
├── main
├── java
└── com
└── demo
└── todolistapiwithfaunadb
├── data
│ ├── dataModel
├── persistence
├── rest
├── service
└── TodolistApiWithFaunadbApplication.java
src/main/java/com/demo/todolistapiwithfaunadb/data/dataModel
directory path create an abstract Entity
class:package com.demo.todolistapiwithfaunadb.data.dataModel;
public abstract class Entity {
protected String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
/todos
GET endpoint which lists all Todos in the database, breaking up the request results into multiple pages makes the request more efficient and quick. This process is called Pagination. To achieve this, create Page
and PaginationOptions
classes.dataModel
package, Page
class:package com.demo.todolistapiwithfaunadb.data.dataModel;
import java.util.List;
import java.util.Optional;
public class Page <T> {
private List<T> data;
private Optional<String> before;
private Optional<String> after;
public Page(List<T> data, Optional<String> before, Optional<String> after) {
this.data = data;
this.before = before;
this.after = after;
}
public List<T> getData() {
return data;
}
public void setData(List<T> data) {
this.data = data;
}
public Optional<String> getBefore() {
return before;
}
public void setBefore(Optional<String> before) {
this.before = before;
}
public Optional<String> getAfter() {
return after;
}
public void setAfter(Optional<String> after) {
this.after = after;
}
}
Page
class creates a new Page with the following given parameters:data
parameter lists the data of the current page sequence.before
optional parameter serves as a cursor that contains the Id of the previous record before the current sequence of data.after
optional parameter serves as a cursor that contains the Id of the next record after the current sequence of data.PaginationOptions
class:package com.demo.todolistapiwithfaunadb.data.dataModel;
import java.util.Optional;
public class PaginationOptions {
private Optional<Integer> size;
private Optional<String> before;
private Optional<String> after;
public PaginationOptions(Optional<Integer> size, Optional<String> before, Optional<String> after) {
this.size = size;
this.before = before;
this.after = after;
}
public Optional<Integer> getSize() {
return size;
}
public void setSize(Optional<Integer> size) {
this.size = size;
}
public Optional<String> getBefore() {
return before;
}
public void setBefore(Optional<String> before) {
this.before = before;
}
public Optional<String> getAfter() {
return after;
}
public void setAfter(Optional<String> after) {
this.after = after;
}
}
PaginationOptions
class creates a new PaginationOptions
object with the following given parameters:size
parameter defines the max number of elements to return on the requested page.before
optional parameter indicates to return the previous page of results before the Id (exclusive).after
optional parameter indicates to return the next page of results after the Id (inclusive).data
package, create a TodoEntity
from the Entity
base class, which will represent a simple Todo for the Todo List API:package com.demo.todolistapiwithfaunadb.data;
import com.demo.todolistapiwithfaunadb.data.dataModel.Entity;
import com.faunadb.client.types.FaunaConstructor;
import com.faunadb.client.types.FaunaField;
public class TodoEntity extends Entity {
@FaunaField
private String title;
@FaunaField
private String description;
@FaunaConstructor
public TodoEntity(@FaunaField("id") String id,
@FaunaField("title") String title,
@FaunaField("description") String description) {
this.id = id;
this.title = title;
this.description = description;
}
public void setTitle(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
TodoEntity
fields annotate with Fauna’s @FaunaField annotation, which maps a field to an object field in FaunaDB when encoding or decoding an object.@FaunaConstructor
specifies the TodoEntity
constructor to be used when decoding objects with a Decoder.NewId()
function, which creates a unique number across the entire Fauna cluster. Due to that, you will need to create a replica of the TodoEntity
containing all necessary data except Id, which will be used for POST (Creating) and PUT (Updating) requests.data
package, create a CreateOrUpdateTodoData
class:package com.demo.todolistapiwithfaunadb.data;
public class CreateOrUpdateTodoData {
private String title;
private String description;
public CreateOrUpdateTodoData(String title, String description) {
this.title = title;
this.description = description;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
FaunaRepository
class, which will serve as the base repository for entity repositories.FaunaRepository
class will implement Repository
and IdentityFactory
interfaces.Repository
interface. This interface defines the base trait for implementing all other repositories and sets base methods for mimicking a collection API:package com.odazie.faunadataapiandjava.persistence;
import com.odazie.faunadataapiandjava.data.model.Entity;
import com.odazie.faunadataapiandjava.data.model.Page;
import com.odazie.faunadataapiandjava.data.model.PaginationOptions;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public interface Repository<T extends Entity> {
// This method saves the given Entity into the Repository.
CompletableFuture<T> save(T entity);
// This method finds an Entity for the given Id
CompletableFuture<Optional<T>> find(String id);
// This method retrieves a Page of TodoEntity entities for the given PaginationOptions
CompletableFuture<Page<T>> findAll(PaginationOptions po);
// This method finds the Entity for the given Id and removes it. If no Entity can be found for the given Id an empty result is returned.
CompletableFuture<Optional<T>> remove(String id);
}
*<*
T extends Entity
*>*
says that the expected T
can contain class objects representing any class that implements the Entity
base class.CompletableFuture
as Fauna’s FQL queries execute concurrently.IdentityFactory
interface:package com.demo.todolistapiwithfaunadb.persistence;
import java.util.concurrent.CompletableFuture;
public interface IdentityFactory {
CompletableFuture<String> nextId();
}
FaunaRepository
class. Still, in the persistence
package, create the FaunaRepository
class:package com.demo.todolistapiwithfaunadb.persistence;
import com.demo.todolistapiwithfaunadb.data.dataModel.Entity;
import com.demo.todolistapiwithfaunadb.data.dataModel.Page;
import com.demo.todolistapiwithfaunadb.data.dataModel.PaginationOptions;
import com.faunadb.client.FaunaClient;
import com.faunadb.client.errors.NotFoundException;
import com.faunadb.client.query.Expr;
import com.faunadb.client.query.Pagination;
import com.faunadb.client.types.Value;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.faunadb.client.query.Language.*;
public abstract class FaunaRepository<T extends Entity> implements Repository<T>, IdentityFactory {
@Autowired
protected FaunaClient faunaClient;
protected final Class<T> entityType;
protected final String collectionName;
protected final String collectionIndexName;
protected FaunaRepository(Class<T> entityType, String collectionName, String collectionIndexName) {
this.entityType = entityType;
this.collectionName = collectionName;
this.collectionIndexName = collectionIndexName;
}
// This method returns a unique valid Id leveraging Fauna's NewId function.
@Override
public CompletableFuture<String> nextId() {
CompletableFuture<String> result =
faunaClient.query(
NewId()
)
.thenApply(value -> value.to(String.class).get());
return result;
}
// This method saves an entity to the database using the saveQuery method below. It also returns the result of the saved entity.
@Override
public CompletableFuture<T> save(T entity) {
CompletableFuture<T> result =
faunaClient.query(
saveQuery(Value(entity.getId()), Value(entity))
)
.thenApply(this::toEntity);
return result;
}
// This method deletes from the data an entity(document) with the specified Id.
@Override
public CompletableFuture<Optional<T>> remove(String id) {
CompletableFuture<T> result =
faunaClient.query(
Select(
Value("data"),
Delete(Ref(Collection(collectionName), Value(id)))
)
)
.thenApply(this::toEntity);
CompletableFuture<Optional<T>> optionalResult = toOptionalResult(result);
return optionalResult;
}
// This method finds an entity by its Id and returns the entity result.
@Override
public CompletableFuture<Optional<T>> find(String id) {
CompletableFuture<T> result =
faunaClient.query(
Select(
Value("data"),
Get(Ref(Collection(collectionName), Value(id)))
)
)
.thenApply(this::toEntity);
CompletableFuture<Optional<T>> optionalResult = toOptionalResult(result);
return optionalResult;
}
// This method returns all entities(documents) in the database collection using the paginationOptions parameters.
@Override
public CompletableFuture<Page<T>> findAll(PaginationOptions po) {
Pagination paginationQuery = Paginate(Match(Index(Value(collectionIndexName))));
po.getSize().ifPresent(size -> paginationQuery.size(size));
po.getAfter().ifPresent(after -> paginationQuery.after(Ref(Collection(collectionName), Value(after))));
po.getBefore().ifPresent(before -> paginationQuery.before(Ref(Collection(collectionName), Value(before))));
CompletableFuture<Page<T>> result =
faunaClient.query(
Map(
paginationQuery,
Lambda(Value("nextRef"), Select(Value("data"), Get(Var("nextRef"))))
)
).thenApply(this::toPage);
return result;
}
// This is the saveQuery expression method used by the save method to persist the database.
protected Expr saveQuery(Expr id, Expr data) {
Expr query =
Select(
Value("data"),
If(
Exists(Ref(Collection(collectionName), id)),
Replace(Ref(Collection(collectionName), id), Obj("data", data)),
Create(Ref(Collection(collectionName), id), Obj("data", data))
)
);
return query;
}
// This method converts a FaunaDB Value into an Entity.
protected T toEntity(Value value) {
return value.to(entityType).get();
}
// This method returns an optionalResult from a CompletableFuture<T> result.
protected CompletableFuture<Optional<T>> toOptionalResult(CompletableFuture<T> result) {
CompletableFuture<Optional<T>> optionalResult =
result.handle((v, t) -> {
CompletableFuture<Optional<T>> r = new CompletableFuture<>();
if(v != null) r.complete(Optional.of(v));
else if(t != null && t.getCause() instanceof NotFoundException) r.complete(Optional.empty());
else r.completeExceptionally(t);
return r;
}).thenCompose(Function.identity());
return optionalResult;
}
// This method converts a FaunaDB Value into a Page with the Entity type.
protected Page<T> toPage(Value value) {
Optional<String> after = value.at("after").asCollectionOf(Value.RefV.class).map(c -> c.iterator().next().getId()).getOptional();
Optional<String> before = value.at("before").asCollectionOf(Value.RefV.class).map(c -> c.iterator().next().getId()).getOptional();
List<T> data = value.at("data").collect(entityType).stream().collect(Collectors.toList());
Page<T> page = new Page(data, before, after);
return page;
}
}
The FaunaClient
is @Autowired
within the FaunaRespository
to run CRUD queries to your Fauna cloud database.
The produced Id for nextId()
method is unique across the entire cluster, as you’ve seen before. For more details, check out Fauna’s documentation on NewId built-in function.
The Select
and Value
functions are present in most of the FaunaRespository
queries. The Select
function extracts a value under the given path. In contrast, the Value
function defines the string value of the operation it performs.
The save()
method uses the transactional saveQuery()
method to perform a valid database save operation. For more details on the transactional query, check out Fauna’s documentation reference on Create, Replace, If, Exists built-in functions.
The remove()
method uses the built-in Delete function to remove/delete a specific entity by Id.
The find()
method uses the Get function to retrieve a single entity(document) by Id.
The findAll()
method using the PaginationOptions
provided paginates all entities(documents) in a collection. The Map applies the given Lambda to each element of the provided collection. It returns the results of each application in a new collection of the same type.
The toOptionalResult()
method in-depth:
CompletableFuture
contains a successful result. In that case, it returns a new CompletableFuture
containing the result wrapped in an Optional instance.CompletableFuture
contains a failing result caused by a NotFoundException
, it returns a new CompletableFuture
.CompletableFuture
contains a failing result caused by a NotFoundException
. In that case, it returns a new CompletableFuture
containing an Optional empty instance.CompletableFuture
contains a failing result caused by any other Throwable
. In that case, it returns a new CompletableFuture
with the same failing result.FaunaRepository
implementation for the TodoEntity
, create a TodoRepository
class:package com.demo.todolistapiwithfaunadb.persistence;
import com.demo.todolistapiwithfaunadb.data.TodoEntity;
import org.springframework.stereotype.Repository;
@Repository
public class TodoRepository extends FaunaRepository<TodoEntity> {
public TodoRepository(){
super(TodoEntity.class, "todos", "all_todos");
}
//-- Custom repository operations specific to the TodoEntity will go below --//
}
The @Repository
annotation indicates that the class TodoRepository
provides the mechanism for storage, retrieval, search, update and delete operation on objects.
super()
invokes the FaunaRepository
constructor to set the collectionName
and indexName
for the TodoEntity
.
In this repository class, you can add custom operations like findByTitle()
specific to the TodoEntity
.
service
package, create a class TodoService
:package com.demo.todolistapiwithfaunadb.service;
import com.demo.todolistapiwithfaunadb.data.CreateOrUpdateTodoData;
import com.demo.todolistapiwithfaunadb.data.TodoEntity;
import com.demo.todolistapiwithfaunadb.data.dataModel.Page;
import com.demo.todolistapiwithfaunadb.data.dataModel.PaginationOptions;
import com.demo.todolistapiwithfaunadb.persistence.TodoRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@Service
public class TodoService {
@Autowired
private TodoRepository todoRepository;
public CompletableFuture<TodoEntity> createTodo(CreateOrUpdateTodoData data) {
CompletableFuture<TodoEntity> result =
todoRepository.nextId()
.thenApply(id -> new TodoEntity(id, data.getTitle(), data.getDescription()))
.thenCompose(todoEntity -> todoRepository.save(todoEntity));
return result;
}
public CompletableFuture<Optional<TodoEntity>> getTodo(String id) {
return todoRepository.find(id);
}
public CompletableFuture<Optional<TodoEntity>> updateTodo(String id, CreateOrUpdateTodoData data) {
CompletableFuture<Optional<TodoEntity>> result =
todoRepository.find(id)
.thenCompose(optionalTodoEntity ->
optionalTodoEntity
.map(todoEntity -> todoRepository.save(new TodoEntity(id, data.getTitle(), data.getDescription())).thenApply(Optional::of))
.orElseGet(() -> CompletableFuture.completedFuture(Optional.empty())));
return result;
}
public CompletableFuture<Optional<TodoEntity>> deleteTodo(String id) {
return todoRepository.remove(id);
}
public CompletableFuture<Page<TodoEntity>> getAllTodos(PaginationOptions po) {
return todoRepository.findAll(po);
}
}
The @Service
indicates that the class TodoService
provides some business functionalities, and the TodoRepository
is @Autowired
in the service.
The createTodo
method builds up a new TodoEntity with the given CreateUpdateTodoData
and a generated valid Id. Then, it saves the new entity into the database.
getTodo()
finds and retrieves a Todo by its Id from the database.
updateTodo()
looks up a Todo for the given Id and replaces it with the given *CreateUpdateTodoData*
, if any.
deleteTodo()
deletes a Todo from the database for the given Id.
getAllTodos()
retrieves a Page of Todos from the database for the given PaginationOptions
.
rest
layer.rest
package, create a class TodoRestController
:package com.demo.todolistapiwithfaunadb.rest;
import com.demo.todolistapiwithfaunadb.data.CreateOrUpdateTodoData;
import com.demo.todolistapiwithfaunadb.data.TodoEntity;
import com.demo.todolistapiwithfaunadb.data.dataModel.Page;
import com.demo.todolistapiwithfaunadb.data.dataModel.PaginationOptions;
import com.demo.todolistapiwithfaunadb.service.TodoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@RestController
public class TodoRestController {
@Autowired
private TodoService todoService;
@PostMapping("/todos")
public CompletableFuture<ResponseEntity> createTodo(@RequestBody CreateOrUpdateTodoData data) {
return todoService.createTodo(data)
.thenApply(todoEntity -> new ResponseEntity(todoEntity, HttpStatus.CREATED));
}
@GetMapping("/todos/{id}")
public CompletableFuture<ResponseEntity> getTodo(@PathVariable("id") String id) {
CompletableFuture<ResponseEntity> result =
todoService.getTodo(id)
.thenApply(optionalTodoEntity ->
optionalTodoEntity
.map(todoEntity -> new ResponseEntity(todoEntity, HttpStatus.OK))
.orElseGet(() -> new ResponseEntity(HttpStatus.NOT_FOUND))
);
return result;
}
@PutMapping("/todos/{id}")
public CompletableFuture<ResponseEntity> updateTodo(@PathVariable("id") String id, @RequestBody CreateOrUpdateTodoData data) {
CompletableFuture<ResponseEntity> result =
todoService.updateTodo(id, data)
.thenApply(optionalTodoEntity ->
optionalTodoEntity
.map(todoEntity -> new ResponseEntity(todoEntity, HttpStatus.OK))
.orElseGet(() -> new ResponseEntity(HttpStatus.NOT_FOUND)
)
);
return result;
}
@DeleteMapping(value = "/todos/{id}")
public CompletableFuture<ResponseEntity> deletePost(@PathVariable("id")String id) {
CompletableFuture<ResponseEntity> result =
todoService.deleteTodo(id)
.thenApply(optionalTodoEntity ->
optionalTodoEntity
.map(todo -> new ResponseEntity(todo, HttpStatus.OK))
.orElseGet(() -> new ResponseEntity(HttpStatus.NOT_FOUND)
)
);
return result;
}
@GetMapping("/todos")
public CompletableFuture<Page<TodoEntity>> getAllTodos(
@RequestParam("size") Optional<Integer> size,
@RequestParam("before") Optional<String> before,
@RequestParam("after") Optional<String> after) {
PaginationOptions po = new PaginationOptions(size, before, after);
CompletableFuture<Page<TodoEntity>> result = todoService.getAllTodos(po);
return result;
}
}
The rest controller method with @PostMapping(
"/todos"
)
mapping creates a new Todo with the given @RequestBody
CreateOrUpdateTodoData
data.
The rest controller methods with @GetMapping(
"/todos/{id}"
)
and @DeleteMapping(
value = "/todos/{id}"
)
mappings retrieves and deletes respectively a Todo with Id bound by @PathVariable(
"
id
"
)
.
The rest controller method with @PutMapping(
"/todos/{id}"
)
mapping updates Todo data with Id bound by @PathVariable("id")
.
The rest controller method with @GetMapping("/todos")
mapping takes three @RequestParam
's (size, before, after) that builds up the PaginationOptions
for the database query.
8080
. Open Postman: