Skip to content

Commit

Permalink
Migration of GraphQL todo example app to guide (#822)
Browse files Browse the repository at this point in the history
* Migration of GraphQL todo example app to guide

See: micronaut-projects/micronaut-graphql#242
Relates to: micronaut-projects/micronaut-graphql#241

* remove tags and add languages to metadata.json

* Add todo graphql guide and add tests

* Switch to postgres and Flyway

* Update screenshot to new ID format

* More asciidoc work

* Polish

* Further cleanups...

* Address feedback

* use common flyway

* use common test

* use callouts

Co-authored-by: Tim Yates <[email protected]>
  • Loading branch information
sdelamo and timyates committed Mar 17, 2022
1 parent 4997009 commit d19f3f0
Show file tree
Hide file tree
Showing 23 changed files with 896 additions and 1 deletion.
1 change: 1 addition & 0 deletions buildSrc/src/main/java/io/micronaut/guides/Category.java
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package example.micronaut;

import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;

import javax.validation.constraints.NotNull;

import static io.micronaut.data.annotation.GeneratedValue.Type.AUTO;

@MappedEntity // <1>
public class Author {

@Id // <1>
@GeneratedValue(AUTO)
private Long id;

@NotNull
private final String username;

public Author(@NotNull String username) {
this.username = username;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getUsername() {
return username;
}
}
Original file line number Diff line number Diff line change
@@ -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 // <1>
public class AuthorDataFetcher implements DataFetcher<CompletionStage<Author>> {

@Override
public CompletionStage<Author> get(DataFetchingEnvironment environment) {
ToDo toDo = environment.getSource();
DataLoader<Long, Author> authorDataLoader = environment.getDataLoader("author"); // <2>
return authorDataLoader.load(toDo.getAuthorId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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 // <1>
public class AuthorDataLoader implements MappedBatchLoader<Long, Author> {

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<Map<Long, Author>> load(Set<Long> keys) {
return CompletableFuture.supplyAsync(() -> authorRepository
.findByIdIn(keys)
.stream()
.collect(toMap(Author::getId, Function.identity())),
executor
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package example.micronaut;

import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.repository.CrudRepository;

import java.util.Collection;
import java.util.Optional;

import static io.micronaut.data.model.query.builder.sql.Dialect.POSTGRES;

@JdbcRepository(dialect = POSTGRES) // <1>
public interface AuthorRepository extends CrudRepository<Author, Long> { // <2>

Optional<Author> findByUsername(String username); // <3>

Collection<Author> findByIdIn(Collection<Long> ids); // <4>

default Author findOrCreate(String username) { // <5>
return findByUsername(username).orElseGet(() -> save(new Author(username)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package example.micronaut;

import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import jakarta.inject.Singleton;

@Singleton // <1>
public class CompleteToDoDataFetcher implements DataFetcher<Boolean> {

private final ToDoRepository toDoRepository;

public CompleteToDoDataFetcher(ToDoRepository toDoRepository) { // <2>
this.toDoRepository = toDoRepository;
}

@Override
public Boolean get(DataFetchingEnvironment env) {
long id = Long.parseLong(env.getArgument("id"));
return toDoRepository
.findById(id) // <3>
.map(this::setCompletedAndUpdate)
.orElse(false);
}

private boolean setCompletedAndUpdate(ToDo todo) {
todo.setCompleted(true); // <4>
toDoRepository.update(todo); // <5>
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package example.micronaut;

import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import jakarta.inject.Singleton;
import javax.transaction.Transactional;

@Singleton // <1>
public class CreateToDoDataFetcher implements DataFetcher<ToDo> {

private final ToDoRepository toDoRepository;
private final AuthorRepository authorRepository;

public CreateToDoDataFetcher(ToDoRepository toDoRepository, // <2>
AuthorRepository authorRepository) {
this.toDoRepository = toDoRepository;
this.authorRepository = authorRepository;
}

@Override
@Transactional
public ToDo get(DataFetchingEnvironment env) {
String title = env.getArgument("title");
String username = env.getArgument("author");

Author author = authorRepository.findOrCreate(username); // <3>

ToDo toDo = new ToDo(title, author.getId());

return toDoRepository.save(toDo); // <4>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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 // <1>
public class DataLoaderRegistryFactory {

private static final Logger LOG = LoggerFactory.getLogger(DataLoaderRegistryFactory.class);

@SuppressWarnings("unused")
@RequestScope // <2>
public DataLoaderRegistry dataLoaderRegistry(AuthorDataLoader authorDataLoader) {
DataLoaderRegistry dataLoaderRegistry = new DataLoaderRegistry();
dataLoaderRegistry.register(
"author",
DataLoader.newMappedDataLoader(authorDataLoader)
); // <3>

LOG.trace("Created new data loader registry");

return dataLoaderRegistry;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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 graphql.schema.idl.errors.SchemaMissingError;
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.InputStream;
import java.io.InputStreamReader;

@Factory // <1>
public class GraphQLFactory {

@Singleton // <2>
public GraphQL graphQL(ResourceResolver resourceResolver,
ToDosDataFetcher toDosDataFetcher,
CreateToDoDataFetcher createToDoDataFetcher,
CompleteToDoDataFetcher completeToDoDataFetcher,
AuthorDataFetcher authorDataFetcher) {
SchemaParser schemaParser = new SchemaParser();
SchemaGenerator schemaGenerator = new SchemaGenerator();

// Load the schema
InputStream schemaDefinition = resourceResolver
.getResourceAsStream("classpath:schema.graphqls")
.orElseThrow(SchemaMissingError::new);

// Parse the schema and merge it into a type registry
TypeDefinitionRegistry typeRegistry = new TypeDefinitionRegistry();
typeRegistry.merge(schemaParser.parse(new BufferedReader(new InputStreamReader(schemaDefinition))));

// Create the runtime wiring.
RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring()
.type("Query", typeWiring -> typeWiring // <3>
.dataFetcher("toDos", toDosDataFetcher))

.type("Mutation", typeWiring -> typeWiring // <4>
.dataFetcher("createToDo", createToDoDataFetcher)
.dataFetcher("completeToDo", completeToDoDataFetcher))

.type("ToDo", typeWiring -> typeWiring // <5>
.dataFetcher("author", authorDataFetcher))

.build();

// Create the executable schema.
GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);

// Return the GraphQL bean.
return GraphQL.newGraphQL(graphQLSchema).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package example.micronaut;

import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;

import javax.validation.constraints.NotNull;

import static io.micronaut.data.annotation.GeneratedValue.Type.AUTO;

@MappedEntity // <1>
public class ToDo {

@Id // <2>
@GeneratedValue(AUTO)
private Long id;

@NotNull
private String title;

private boolean completed;

private final long authorId;

public ToDo(String title, long authorId) {
this.title = title;
this.authorId = authorId;
}

public Long getId() {
return id;
}

public void setId(Long 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 long getAuthorId() {
return authorId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package example.micronaut;

import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.PageableRepository;

import static io.micronaut.data.model.query.builder.sql.Dialect.POSTGRES;

@JdbcRepository(dialect = POSTGRES) // <1>
public interface ToDoRepository extends PageableRepository<ToDo, Long> { // <2>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package example.micronaut;

import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import jakarta.inject.Singleton;

@Singleton // <1>
public class ToDosDataFetcher implements DataFetcher<Iterable<ToDo>> {

private final ToDoRepository toDoRepository;

public ToDosDataFetcher(ToDoRepository toDoRepository) { // <2>
this.toDoRepository = toDoRepository;
}

@Override
public Iterable<ToDo> get(DataFetchingEnvironment env) {
return toDoRepository.findAll();
}
}
Loading

0 comments on commit d19f3f0

Please sign in to comment.