diff --git a/persistence-modules/fauna/.gitignore b/persistence-modules/fauna/.gitignore
new file mode 100644
index 0000000000..c37fa0c4b3
--- /dev/null
+++ b/persistence-modules/fauna/.gitignore
@@ -0,0 +1 @@
+/application.properties
diff --git a/persistence-modules/fauna/pom.xml b/persistence-modules/fauna/pom.xml
new file mode 100644
index 0000000000..67aabb7501
--- /dev/null
+++ b/persistence-modules/fauna/pom.xml
@@ -0,0 +1,56 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.6.2
+
+
+ com.baeldung
+ fauna-blog
+ 0.0.1-SNAPSHOT
+ fauna-blog
+ Blogging Service built with FaunaDB
+
+ 17
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ com.faunadb
+ faunadb-java
+ 4.2.0
+ compile
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/FaunaBlogApplication.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/FaunaBlogApplication.java
new file mode 100644
index 0000000000..12739342bf
--- /dev/null
+++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/FaunaBlogApplication.java
@@ -0,0 +1,13 @@
+package com.baeldung.faunablog;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class FaunaBlogApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(FaunaBlogApplication.class, args);
+ }
+
+}
diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/FaunaConfiguration.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/FaunaConfiguration.java
new file mode 100644
index 0000000000..9964431475
--- /dev/null
+++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/FaunaConfiguration.java
@@ -0,0 +1,25 @@
+package com.baeldung.faunablog;
+
+import com.faunadb.client.FaunaClient;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.net.MalformedURLException;
+
+@Configuration
+class FaunaConfiguration {
+ @Value("https://db.${fauna.region}.fauna.com/")
+ private String faunaUrl;
+
+ @Value("${fauna.secret}")
+ private String faunaSecret;
+
+ @Bean
+ FaunaClient getFaunaClient() throws MalformedURLException {
+ return FaunaClient.builder()
+ .withEndpoint(faunaUrl)
+ .withSecret(faunaSecret)
+ .build();
+ }
+}
diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/WebSecurityConfiguration.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/WebSecurityConfiguration.java
new file mode 100644
index 0000000000..da99b7578e
--- /dev/null
+++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/WebSecurityConfiguration.java
@@ -0,0 +1,35 @@
+package com.baeldung.faunablog;
+
+import com.baeldung.faunablog.users.FaunaUserDetailsService;
+import com.faunadb.client.FaunaClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.core.userdetails.UserDetailsService;
+
+@Configuration
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
+
+ @Autowired
+ private FaunaClient faunaClient;
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http.csrf().disable();
+ http.authorizeRequests()
+ .antMatchers("/**").permitAll()
+ .and().httpBasic();
+ }
+
+ @Bean
+ @Override
+ public UserDetailsService userDetailsService() {
+ return new FaunaUserDetailsService(faunaClient);
+ }
+}
diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/Author.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/Author.java
new file mode 100644
index 0000000000..ec4854621d
--- /dev/null
+++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/Author.java
@@ -0,0 +1,3 @@
+package com.baeldung.faunablog.posts;
+
+public record Author(String username, String name) {}
diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/Post.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/Post.java
new file mode 100644
index 0000000000..62b6558a37
--- /dev/null
+++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/Post.java
@@ -0,0 +1,5 @@
+package com.baeldung.faunablog.posts;
+
+import java.time.Instant;
+
+public record Post(String id, String title, String content, Author author, Instant created, Long version) {}
diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/PostsController.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/PostsController.java
new file mode 100644
index 0000000000..e8e6316ea8
--- /dev/null
+++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/PostsController.java
@@ -0,0 +1,50 @@
+package com.baeldung.faunablog.posts;
+
+import com.faunadb.client.errors.NotFoundException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+@RestController
+@RequestMapping("/posts")
+public class PostsController {
+ @Autowired
+ private PostsService postsService;
+
+ @GetMapping
+ public List listPosts(@RequestParam(value = "author", required = false) String author) throws ExecutionException, InterruptedException {
+ return author == null ? postsService.getAllPosts() : postsService.getAuthorPosts("graham");
+ }
+
+ @GetMapping("/{id}")
+ public Post getPost(@PathVariable("id") String id, @RequestParam(value = "before", required = false) Long before)
+ throws ExecutionException, InterruptedException {
+ return postsService.getPost(id, before);
+ }
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ @PreAuthorize("isAuthenticated()")
+ public void createPost(@RequestBody UpdatedPost post) throws ExecutionException, InterruptedException {
+ String name = SecurityContextHolder.getContext().getAuthentication().getName();
+ postsService.createPost(name, post.title(), post.content());
+ }
+
+ @PutMapping("/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ @PreAuthorize("isAuthenticated()")
+ public void updatePost(@PathVariable("id") String id, @RequestBody UpdatedPost post)
+ throws ExecutionException, InterruptedException {
+ postsService.updatePost(id, post.title(), post.content());
+ }
+
+ @ExceptionHandler(NotFoundException.class)
+ @ResponseStatus(HttpStatus.NOT_FOUND)
+ public void postNotFound() {}
+}
diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/PostsService.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/PostsService.java
new file mode 100644
index 0000000000..5143a24b28
--- /dev/null
+++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/PostsService.java
@@ -0,0 +1,124 @@
+package com.baeldung.faunablog.posts;
+
+import com.faunadb.client.FaunaClient;
+import com.faunadb.client.types.Value;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.time.Instant;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static com.faunadb.client.query.Language.*;
+
+@Component
+public class PostsService {
+ @Autowired
+ private FaunaClient faunaClient;
+
+ Post getPost(String id, Long before) throws ExecutionException, InterruptedException {
+ var query = Get(Ref(Collection("posts"), id));
+ if (before != null) {
+ query = At(Value(before - 1), query);
+ }
+
+ var postResult= faunaClient.query(
+ Let(
+ "post", query
+ ).in(
+ Obj(
+ "post", Var("post"),
+ "author", Get(Select(Arr(Value("data"), Value("authorRef")), Var("post")))
+ )
+ )).get();
+
+ return parsePost(postResult);
+ }
+
+ List getAllPosts() throws ExecutionException, InterruptedException {
+ var postsResult = faunaClient.query(Map(
+ Paginate(
+ Join(
+ Documents(Collection("posts")),
+ Index("posts_sort_by_created_desc")
+ )
+ ),
+ Lambda(
+ Arr(Value("extra"), Value("ref")),
+ Obj(
+ "post", Get(Var("ref")),
+ "author", Get(Select(Arr(Value("data"), Value("authorRef")), Get(Var("ref"))))
+ )
+ )
+ )).get();
+
+ var posts = postsResult.at("data").asCollectionOf(Value.class).get();
+ return posts.stream().map(this::parsePost).collect(Collectors.toList());
+ }
+
+ List getAuthorPosts(String author) throws ExecutionException, InterruptedException {
+ var postsResult = faunaClient.query(Map(
+ Paginate(
+ Join(
+ Match(Index("posts_by_author"), Select(Value("ref"), Get(Match(Index("users_by_username"), Value(author))))),
+ Index("posts_sort_by_created_desc")
+ )
+ ),
+ Lambda(
+ Arr(Value("extra"), Value("ref")),
+ Obj(
+ "post", Get(Var("ref")),
+ "author", Get(Select(Arr(Value("data"), Value("authorRef")), Get(Var("ref"))))
+ )
+ )
+ )).get();
+
+ var posts = postsResult.at("data").asCollectionOf(Value.class).get();
+ return posts.stream().map(this::parsePost).collect(Collectors.toList());
+ }
+
+ public void createPost(String author, String title, String contents) throws ExecutionException, InterruptedException {
+ faunaClient.query(
+ Create(Collection("posts"),
+ Obj(
+ "data", Obj(
+ "title", Value(title),
+ "contents", Value(contents),
+ "created", Now(),
+ "authorRef", Select(Value("ref"), Get(Match(Index("users_by_username"), Value(author)))))
+ )
+ )
+ ).get();
+ }
+
+ public void updatePost(String id, String title, String contents) throws ExecutionException, InterruptedException {
+ faunaClient.query(
+ Update(Ref(Collection("posts"), id),
+ Obj(
+ "data", Obj(
+ "title", Value(title),
+ "contents", Value(contents))
+ )
+ )
+ ).get();
+ }
+
+ private Post parsePost(Value entry) {
+ var author = entry.at("author");
+ var post = entry.at("post");
+
+ return new Post(
+ post.at("ref").to(Value.RefV.class).get().getId(),
+ post.at("data", "title").to(String.class).get(),
+ post.at("data", "contents").to(String.class).get(),
+ new Author(
+ author.at("data", "username").to(String.class).get(),
+ author.at("data", "name").to(String.class).get()
+ ),
+ post.at("data", "created").to(Instant.class).get(),
+ post.at("ts").to(Long.class).get()
+ );
+ }
+}
diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/UpdatedPost.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/UpdatedPost.java
new file mode 100644
index 0000000000..9850cd5927
--- /dev/null
+++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/posts/UpdatedPost.java
@@ -0,0 +1,3 @@
+package com.baeldung.faunablog.posts;
+
+public record UpdatedPost(String title, String content) {}
diff --git a/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/users/FaunaUserDetailsService.java b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/users/FaunaUserDetailsService.java
new file mode 100644
index 0000000000..2e88aaa477
--- /dev/null
+++ b/persistence-modules/fauna/src/main/java/com/baeldung/faunablog/users/FaunaUserDetailsService.java
@@ -0,0 +1,44 @@
+package com.baeldung.faunablog.users;
+
+import com.faunadb.client.FaunaClient;
+import com.faunadb.client.types.Value;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+
+import java.util.concurrent.ExecutionException;
+
+import static com.faunadb.client.query.Language.*;
+
+public class FaunaUserDetailsService implements UserDetailsService {
+ private FaunaClient faunaClient;
+
+ public FaunaUserDetailsService(FaunaClient faunaClient) {
+ this.faunaClient = faunaClient;
+ }
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ try {
+ Value user = faunaClient.query(Map(
+ Paginate(Match(Index("users_by_username"), Value(username))),
+ Lambda(Value("user"), Get(Var("user")))))
+ .get();
+
+ Value userData = user.at("data").at(0).orNull();
+ if (userData == null) {
+ throw new UsernameNotFoundException("User not found");
+ }
+
+ return User.withDefaultPasswordEncoder()
+ .username(userData.at("data", "username").to(String.class).orNull())
+ .password(userData.at("data", "password").to(String.class).orNull())
+ .roles("USER")
+ .build();
+ } catch (ExecutionException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
+
diff --git a/persistence-modules/fauna/src/main/resources/application.properties b/persistence-modules/fauna/src/main/resources/application.properties
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/pom.xml b/pom.xml
index 1243eb4522..197e946974 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1347,6 +1347,7 @@
spring-boot-modules/spring-boot-cassandre
spring-boot-modules/spring-boot-camel
testing-modules/testing-assertions
+ persistence-modules/fauna
@@ -1409,6 +1410,7 @@
spring-boot-modules/spring-boot-cassandre
spring-boot-modules/spring-boot-camel
testing-modules/testing-assertions
+ persistence-modules/fauna