diff --git a/buildSrc/src/main/java/io/micronaut/guides/Category.java b/buildSrc/src/main/java/io/micronaut/guides/Category.java index a475cb98a7..d4fa26dae2 100644 --- a/buildSrc/src/main/java/io/micronaut/guides/Category.java +++ b/buildSrc/src/main/java/io/micronaut/guides/Category.java @@ -6,6 +6,7 @@ public enum Category { EMAIL("Email"), DATA_ACCESS("Data Access"), CACHE("Cache"), + GRAPHQL("GraphQL"), MESSAGING("Messaging"), SECURITY("Micronaut Security"), SERVICE_DISCOVERY("Service Discovery"), diff --git a/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/Author.java b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/Author.java new file mode 100644 index 0000000000..af4316d3f3 --- /dev/null +++ b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/Author.java @@ -0,0 +1,21 @@ +package example.micronaut; + +public class Author { + + private final String id; + private final String username; + + public Author(String id, String username) { + this.id = id; + this.username = username; + } + + public String getId() { + return id; + } + + public String getUsername() { + return username; + } + +} diff --git a/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/AuthorDataFetcher.java b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/AuthorDataFetcher.java new file mode 100644 index 0000000000..fd37305414 --- /dev/null +++ b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/AuthorDataFetcher.java @@ -0,0 +1,19 @@ +package example.micronaut; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import jakarta.inject.Singleton; +import org.dataloader.DataLoader; +import java.util.concurrent.CompletionStage; + +@Singleton +public class AuthorDataFetcher implements DataFetcher> { + + @Override + public CompletionStage get(DataFetchingEnvironment environment) throws Exception { + ToDo toDo = environment.getSource(); + DataLoader authorDataLoader = environment.getDataLoader("author"); + return authorDataLoader.load(toDo.getAuthorId()); + } + +} diff --git a/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/AuthorDataLoader.java b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/AuthorDataLoader.java new file mode 100644 index 0000000000..cf78d3e38c --- /dev/null +++ b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/AuthorDataLoader.java @@ -0,0 +1,38 @@ +package example.micronaut; + +import io.micronaut.scheduling.TaskExecutors; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import org.dataloader.MappedBatchLoader; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutorService; +import java.util.function.Function; + +import static java.util.stream.Collectors.toMap; + +@Singleton +public class AuthorDataLoader implements MappedBatchLoader { + + private final AuthorRepository authorRepository; + private final ExecutorService executor; + + public AuthorDataLoader(AuthorRepository authorRepository, + @Named(TaskExecutors.IO) ExecutorService executor) { + this.authorRepository = authorRepository; + this.executor = executor; + } + + @Override + public CompletionStage> load(Set keys) { + return CompletableFuture.supplyAsync(() -> + authorRepository + .findAllById(keys) + .stream() + .collect(toMap(Author::getId, Function.identity())), executor); + } + +} diff --git a/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/AuthorRepository.java b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/AuthorRepository.java new file mode 100644 index 0000000000..b6c716f530 --- /dev/null +++ b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/AuthorRepository.java @@ -0,0 +1,38 @@ +package example.micronaut; + +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +@Singleton +public class AuthorRepository { + + private static final Logger LOG = LoggerFactory.getLogger(AuthorRepository.class); + + private final Map authors = new HashMap<>(); + + public List findAllById(Collection ids) { + LOG.debug("Batch loading authors: {}", ids); + + return authors.values() + .stream() + .filter(it -> ids.contains(it.getId())) + .collect(Collectors.toList()); + } + + public Author findOrCreate(String username) { + if (!authors.containsKey(username)) { + authors.put(username, new Author(UUID.randomUUID().toString(), username)); + } + + return authors.get(username); + } + +} diff --git a/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/CompleteToDoDataFetcher.java b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/CompleteToDoDataFetcher.java new file mode 100644 index 0000000000..719a5e1049 --- /dev/null +++ b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/CompleteToDoDataFetcher.java @@ -0,0 +1,28 @@ +package example.micronaut; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import jakarta.inject.Singleton; + +@Singleton +public class CompleteToDoDataFetcher implements DataFetcher { + + private final ToDoRepository toDoRepository; + + public CompleteToDoDataFetcher(ToDoRepository toDoRepository) { + this.toDoRepository = toDoRepository; + } + + @Override + public Boolean get(DataFetchingEnvironment env) { + String id = env.getArgument("id"); + ToDo toDo = toDoRepository.findById(id); + if (toDo != null) { + toDo.setCompleted(true); + toDoRepository.save(toDo); + return true; + } else { + return false; + } + } +} diff --git a/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/CreateToDoDataFetcher.java b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/CreateToDoDataFetcher.java new file mode 100644 index 0000000000..00d3f123b9 --- /dev/null +++ b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/CreateToDoDataFetcher.java @@ -0,0 +1,26 @@ +package example.micronaut; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import jakarta.inject.Singleton; + +@Singleton +public class CreateToDoDataFetcher implements DataFetcher { + + private final ToDoRepository toDoRepository; + private final AuthorRepository authorRepository; + + public CreateToDoDataFetcher(ToDoRepository toDoRepository, AuthorRepository authorRepository) { + this.toDoRepository = toDoRepository; + this.authorRepository = authorRepository; + } + + @Override + public ToDo get(DataFetchingEnvironment env) { + String title = env.getArgument("title"); + String username = env.getArgument("author"); + Author author = authorRepository.findOrCreate(username); + ToDo toDo = new ToDo(title, author.getId()); + return toDoRepository.save(toDo); + } +} diff --git a/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/DataLoaderRegistryFactory.java b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/DataLoaderRegistryFactory.java new file mode 100644 index 0000000000..ef0f207163 --- /dev/null +++ b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/DataLoaderRegistryFactory.java @@ -0,0 +1,30 @@ +package example.micronaut; + +import io.micronaut.context.annotation.Factory; +import io.micronaut.runtime.http.scope.RequestScope; +import org.dataloader.DataLoader; +import org.dataloader.DataLoaderRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Factory +public class DataLoaderRegistryFactory { + + private static final Logger LOG = LoggerFactory.getLogger(DataLoaderRegistryFactory.class); + + private final AuthorDataLoader authorDataLoader; + + public DataLoaderRegistryFactory(AuthorDataLoader authorDataLoader) { + this.authorDataLoader = authorDataLoader; + } + + @SuppressWarnings("unused") + @RequestScope + public DataLoaderRegistry dataLoaderRegistry() { + DataLoaderRegistry dataLoaderRegistry = new DataLoaderRegistry(); + dataLoaderRegistry.register("author", DataLoader.newMappedDataLoader(authorDataLoader)); + LOG.trace("Created new data loader registry"); + return dataLoaderRegistry; + } + +} diff --git a/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/DeleteToDoDataFetcher.java b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/DeleteToDoDataFetcher.java new file mode 100644 index 0000000000..4b31fd2f70 --- /dev/null +++ b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/DeleteToDoDataFetcher.java @@ -0,0 +1,27 @@ +package example.micronaut; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import jakarta.inject.Singleton; + +@Singleton +public class DeleteToDoDataFetcher implements DataFetcher { + + private final ToDoRepository toDoRepository; + + public DeleteToDoDataFetcher(ToDoRepository toDoRepository) { + this.toDoRepository = toDoRepository; + } + + @Override + public Boolean get(DataFetchingEnvironment env) { + String id = env.getArgument("id"); + ToDo toDo = toDoRepository.findById(id); + if (toDo != null) { + toDoRepository.deleteById(id); + return true; + } else { + return false; + } + } +} diff --git a/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/GraphQLFactory.java b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/GraphQLFactory.java new file mode 100644 index 0000000000..dbd923aac0 --- /dev/null +++ b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/GraphQLFactory.java @@ -0,0 +1,55 @@ +package example.micronaut; + +import graphql.GraphQL; +import graphql.schema.GraphQLSchema; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.Factory; +import io.micronaut.core.io.ResourceResolver; +import jakarta.inject.Singleton; + +import java.io.BufferedReader; +import java.io.InputStreamReader; + +@Factory +public class GraphQLFactory { + + @Bean + @Singleton + public GraphQL graphQL(ResourceResolver resourceResolver, + ToDosDataFetcher toDosDataFetcher, + CreateToDoDataFetcher createToDoDataFetcher, + CompleteToDoDataFetcher completeToDoDataFetcher, + DeleteToDoDataFetcher deleteToDoDataFetcher, + AuthorDataFetcher authorDataFetcher) { + + SchemaParser schemaParser = new SchemaParser(); + SchemaGenerator schemaGenerator = new SchemaGenerator(); + + // Parse the schema. + TypeDefinitionRegistry typeRegistry = new TypeDefinitionRegistry(); + typeRegistry.merge(schemaParser.parse(new BufferedReader(new InputStreamReader( + resourceResolver.getResourceAsStream("classpath:schema.graphqls").get())))); + + // Create the runtime wiring. + RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type("Query", typeWiring -> typeWiring + .dataFetcher("toDos", toDosDataFetcher)) + .type("Mutation", typeWiring -> typeWiring + .dataFetcher("createToDo", createToDoDataFetcher) + .dataFetcher("completeToDo", completeToDoDataFetcher) + .dataFetcher("deleteToDo", deleteToDoDataFetcher)) + .type("ToDo", typeWiring -> typeWiring + .dataFetcher("author", authorDataFetcher)) + .build(); + + // Create the executable schema. + GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring); + + // Return the GraphQL bean. + return GraphQL.newGraphQL(graphQLSchema).build(); + } +} diff --git a/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/ToDo.java b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/ToDo.java new file mode 100644 index 0000000000..cee5b8cf0c --- /dev/null +++ b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/ToDo.java @@ -0,0 +1,46 @@ +package example.micronaut; + +public class ToDo { + + private String id; + private String title; + private boolean completed; + private String authorId; + + public ToDo(String title, String authorId) { + this.title = title; + this.authorId = authorId; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public boolean isCompleted() { + return completed; + } + + public void setCompleted(boolean completed) { + this.completed = completed; + } + + public String getAuthorId() { + return authorId; + } + + public void setAuthorId(String authorId) { + this.authorId = authorId; + } +} diff --git a/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/ToDoRepository.java b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/ToDoRepository.java new file mode 100644 index 0000000000..cb89872ae7 --- /dev/null +++ b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/ToDoRepository.java @@ -0,0 +1,39 @@ +package example.micronaut; + +import jakarta.inject.Singleton; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +@Singleton +public class ToDoRepository { + + private final Map toDos = new LinkedHashMap<>(); + + public ToDoRepository(AuthorRepository authorRepository) { + save(new ToDo("Book flights to Gran Canaria", authorRepository.findOrCreate("William").getId())); + save(new ToDo("Order torrefacto coffee beans", authorRepository.findOrCreate("George").getId())); + save(new ToDo("Watch La Casa de Papel", authorRepository.findOrCreate("George").getId())); + } + + public Iterable findAll() { + return toDos.values(); + } + + public ToDo findById(String id) { + return toDos.get(id); + } + + public ToDo save(ToDo toDo) { + if (toDo.getId() == null) { + toDo.setId(UUID.randomUUID().toString()); + } + toDos.put(toDo.getId(), toDo); + return toDo; + } + + public void deleteById(String id) { + toDos.remove(id); + } +} diff --git a/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/ToDosDataFetcher.java b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/ToDosDataFetcher.java new file mode 100644 index 0000000000..a529eb5618 --- /dev/null +++ b/guides/micronaut-graphql-todo/java/src/main/java/example/micronaut/ToDosDataFetcher.java @@ -0,0 +1,20 @@ +package example.micronaut; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import jakarta.inject.Singleton; + +@Singleton +public class ToDosDataFetcher implements DataFetcher> { + + private final ToDoRepository toDoRepository; + + public ToDosDataFetcher(ToDoRepository toDoRepository) { + this.toDoRepository = toDoRepository; + } + + @Override + public Iterable get(DataFetchingEnvironment env) { + return toDoRepository.findAll(); + } +} diff --git a/guides/micronaut-graphql-todo/java/src/main/resources/application.yml b/guides/micronaut-graphql-todo/java/src/main/resources/application.yml new file mode 100644 index 0000000000..1c7d7e41a7 --- /dev/null +++ b/guides/micronaut-graphql-todo/java/src/main/resources/application.yml @@ -0,0 +1,4 @@ +#tag::graphql[] +graphql: + graphiql.enabled: true +#end::graphql[] \ No newline at end of file diff --git a/guides/micronaut-graphql-todo/java/src/main/resources/schema.graphqls b/guides/micronaut-graphql-todo/java/src/main/resources/schema.graphqls new file mode 100644 index 0000000000..fa2c959aa9 --- /dev/null +++ b/guides/micronaut-graphql-todo/java/src/main/resources/schema.graphqls @@ -0,0 +1,21 @@ +type Query { + toDos: [ToDo!]! +} + +type Mutation { + createToDo(title: String!, author: String!): ToDo + completeToDo(id: ID!): Boolean! + deleteToDo(id: ID!): Boolean! +} + +type ToDo { + id: ID! + title: String! + completed: Boolean! + author: Author! +} + +type Author { + id: ID! + username: String! +} diff --git a/guides/micronaut-graphql-todo/metadata.json b/guides/micronaut-graphql-todo/metadata.json new file mode 100644 index 0000000000..a0bbcbf1b1 --- /dev/null +++ b/guides/micronaut-graphql-todo/metadata.json @@ -0,0 +1,14 @@ +{ + "title": "TODO application with Micronaut GraphQL", + "intro": "Build a TODO application with Micronaut GraphQL.", + "authors": ["Sergio del Amo", "Tim Yates"], + "tags": ["graphql"], + "category": "GraphQL", + "publicationDate": "2022-03-04", + "apps": [ + { + "name": "default", + "features": ["graalvm", "graphql"] + } + ] +} diff --git a/guides/micronaut-graphql-todo/micronaut-graphql-todo.adoc b/guides/micronaut-graphql-todo/micronaut-graphql-todo.adoc new file mode 100644 index 0000000000..f16557028a --- /dev/null +++ b/guides/micronaut-graphql-todo/micronaut-graphql-todo.adoc @@ -0,0 +1,47 @@ +common:header.adoc[] + +common:requirements.adoc[] + +common:completesolution.adoc[] + +common:create-app-features.adoc[] + +source:Author[] + +source:AuthorDataFetcher[] + +source:AuthorDataLoader[] + +source:AuthorRepository[] + +source:CompleteTodoDataFetcher[] + +source:DataLoaderRegistryFactory[] + +source:DeleteTodoDataFetcher[] + +source:GraphQLFactory[] + +source:ToDo[] + +source:ToDoRepository[] + +source:ToDosDataFetcher[] + +resource:application.yml[tag=graphql] + +resource:schema.graphqls[] + +common:testApp.adoc[] + +Open the embedded [GraphiQL](http://localhost:8080/graphiql) IDE to interact with the GraphQL To-Do API. + +common:runapp.adoc[] + +common:graal-with-plugins.adoc[] + +== Next steps + +Read more about https://micronaut-projects.github.io/micronaut-graphql/latest/guide/[Micronaut GraphQL]. + +common:helpWithMicronaut.adoc[] diff --git a/guides/micronaut-graphql/metadata.json b/guides/micronaut-graphql/metadata.json index 0a60dfcd78..ef0430f2e2 100644 --- a/guides/micronaut-graphql/metadata.json +++ b/guides/micronaut-graphql/metadata.json @@ -3,7 +3,7 @@ "intro": "Learn how to use Micronaut GraphQL.", "authors": ["Iván López"], "tags": ["graphql"], - "category": "Getting Started", + "category": "GraphQL", "publicationDate": "2021-05-21", "apps": [ {