feat: add graphql

This commit is contained in:
xushanchuan 2021-03-19 15:24:10 +08:00
parent 01fac42c64
commit 44a70270c8
No known key found for this signature in database
GPG Key ID: 44D23C44E00838D6
11 changed files with 1194 additions and 0 deletions

View File

@ -0,0 +1,3 @@
package io.spring.graphql;
public class AuthenticationException extends RuntimeException {}

View File

@ -0,0 +1,386 @@
package io.spring.graphql.users;
import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsData;
import com.netflix.graphql.dgs.DgsDataFetchingEnvironment;
import com.netflix.graphql.dgs.InputArgument;
import graphql.execution.DataFetcherResult;
import graphql.relay.DefaultConnectionCursor;
import graphql.relay.DefaultPageInfo;
import graphql.schema.DataFetchingEnvironment;
import io.spring.Util;
import io.spring.api.exception.ResourceNotFoundException;
import io.spring.application.ArticleQueryService;
import io.spring.application.CursorPageParameter;
import io.spring.application.CursorPager;
import io.spring.application.CursorPager.Direction;
import io.spring.application.data.ArticleData;
import io.spring.application.data.CommentData;
import io.spring.core.user.User;
import io.spring.core.user.UserRepository;
import io.spring.graphql.DgsConstants;
import io.spring.graphql.DgsConstants.ARTICLEPAYLOAD;
import io.spring.graphql.DgsConstants.COMMENT;
import io.spring.graphql.DgsConstants.PROFILE;
import io.spring.graphql.DgsConstants.QUERY;
import io.spring.graphql.types.Article;
import io.spring.graphql.types.ArticleEdge;
import io.spring.graphql.types.ArticlesConnection;
import io.spring.graphql.types.Profile;
import java.util.HashMap;
import java.util.stream.Collectors;
import org.joda.time.format.ISODateTimeFormat;
import org.springframework.beans.factory.annotation.Autowired;
@DgsComponent
public class ArticleDatafetcher {
private ArticleQueryService articleQueryService;
private UserRepository userRepository;
@Autowired
public ArticleDatafetcher(
ArticleQueryService articleQueryService, UserRepository userRepository) {
this.articleQueryService = articleQueryService;
this.userRepository = userRepository;
}
@DgsData(parentType = DgsConstants.QUERY_TYPE, field = QUERY.Feed)
public DataFetcherResult<ArticlesConnection> getFeed(
@InputArgument("first") Integer first,
@InputArgument("after") String after,
@InputArgument("last") Integer last,
@InputArgument("before") String before,
DgsDataFetchingEnvironment dfe) {
if (first == null && last == null) {
throw new IllegalArgumentException("first 和 last 必须只存在一个");
}
User current = SecurityUtil.getCurrentUser().orElse(null);
CursorPager<ArticleData> articles;
if (first != null) {
articles =
articleQueryService.findUserFeedWithCursor(
current, new CursorPageParameter(after, first, Direction.NEXT));
} else {
articles =
articleQueryService.findUserFeedWithCursor(
current, new CursorPageParameter(before, last, Direction.PREV));
}
graphql.relay.PageInfo pageInfo = buildArticlePageInfo(articles);
ArticlesConnection articlesConnection =
ArticlesConnection.newBuilder()
.pageInfo(pageInfo)
.edges(
articles.getData().stream()
.map(
a ->
ArticleEdge.newBuilder()
.cursor(a.getCursor())
.node(buildArticleResult(a))
.build())
.collect(Collectors.toList()))
.build();
return DataFetcherResult.<ArticlesConnection>newResult()
.data(articlesConnection)
.localContext(
articles.getData().stream().collect(Collectors.toMap(ArticleData::getSlug, a -> a)))
.build();
}
@DgsData(parentType = PROFILE.TYPE_NAME, field = PROFILE.Feed)
public DataFetcherResult<ArticlesConnection> userFeed(
@InputArgument("first") Integer first,
@InputArgument("after") String after,
@InputArgument("last") Integer last,
@InputArgument("before") String before,
DgsDataFetchingEnvironment dfe) {
if (first == null && last == null) {
throw new IllegalArgumentException("first 和 last 必须只存在一个");
}
Profile profile = dfe.getSource();
User target =
userRepository
.findByUsername(profile.getUsername())
.orElseThrow(ResourceNotFoundException::new);
CursorPager<ArticleData> articles;
if (first != null) {
articles =
articleQueryService.findUserFeedWithCursor(
target, new CursorPageParameter(after, first, Direction.NEXT));
} else {
articles =
articleQueryService.findUserFeedWithCursor(
target, new CursorPageParameter(before, last, Direction.PREV));
}
graphql.relay.PageInfo pageInfo = buildArticlePageInfo(articles);
ArticlesConnection articlesConnection =
ArticlesConnection.newBuilder()
.pageInfo(pageInfo)
.edges(
articles.getData().stream()
.map(
a ->
ArticleEdge.newBuilder()
.cursor(a.getCursor())
.node(buildArticleResult(a))
.build())
.collect(Collectors.toList()))
.build();
return DataFetcherResult.<ArticlesConnection>newResult()
.data(articlesConnection)
.localContext(
articles.getData().stream().collect(Collectors.toMap(ArticleData::getSlug, a -> a)))
.build();
}
@DgsData(parentType = PROFILE.TYPE_NAME, field = PROFILE.Favorites)
public DataFetcherResult<ArticlesConnection> userFavorites(
@InputArgument("first") Integer first,
@InputArgument("after") String after,
@InputArgument("last") Integer last,
@InputArgument("before") String before,
DgsDataFetchingEnvironment dfe) {
if (first == null && last == null) {
throw new IllegalArgumentException("first 和 last 必须只存在一个");
}
User current = SecurityUtil.getCurrentUser().orElse(null);
Profile profile = dfe.getSource();
CursorPager<ArticleData> articles;
if (first != null) {
articles =
articleQueryService.findRecentArticlesWithCursor(
null,
null,
profile.getUsername(),
new CursorPageParameter(after, first, Direction.NEXT),
current);
} else {
articles =
articleQueryService.findRecentArticlesWithCursor(
null,
null,
profile.getUsername(),
new CursorPageParameter(before, last, Direction.PREV),
current);
}
graphql.relay.PageInfo pageInfo = buildArticlePageInfo(articles);
ArticlesConnection articlesConnection =
ArticlesConnection.newBuilder()
.pageInfo(pageInfo)
.edges(
articles.getData().stream()
.map(
a ->
ArticleEdge.newBuilder()
.cursor(a.getCursor())
.node(buildArticleResult(a))
.build())
.collect(Collectors.toList()))
.build();
return DataFetcherResult.<ArticlesConnection>newResult()
.data(articlesConnection)
.localContext(
articles.getData().stream().collect(Collectors.toMap(ArticleData::getSlug, a -> a)))
.build();
}
@DgsData(parentType = PROFILE.TYPE_NAME, field = PROFILE.Articles)
public DataFetcherResult<ArticlesConnection> userArticles(
@InputArgument("first") Integer first,
@InputArgument("after") String after,
@InputArgument("last") Integer last,
@InputArgument("before") String before,
DgsDataFetchingEnvironment dfe) {
if (first == null && last == null) {
throw new IllegalArgumentException("first 和 last 必须只存在一个");
}
User current = SecurityUtil.getCurrentUser().orElse(null);
Profile profile = dfe.getSource();
CursorPager<ArticleData> articles;
if (first != null) {
articles =
articleQueryService.findRecentArticlesWithCursor(
null,
profile.getUsername(),
null,
new CursorPageParameter(after, first, Direction.NEXT),
current);
} else {
articles =
articleQueryService.findRecentArticlesWithCursor(
null,
profile.getUsername(),
null,
new CursorPageParameter(before, last, Direction.PREV),
current);
}
graphql.relay.PageInfo pageInfo = buildArticlePageInfo(articles);
ArticlesConnection articlesConnection =
ArticlesConnection.newBuilder()
.pageInfo(pageInfo)
.edges(
articles.getData().stream()
.map(
a ->
ArticleEdge.newBuilder()
.cursor(a.getCursor())
.node(buildArticleResult(a))
.build())
.collect(Collectors.toList()))
.build();
return DataFetcherResult.<ArticlesConnection>newResult()
.data(articlesConnection)
.localContext(
articles.getData().stream().collect(Collectors.toMap(ArticleData::getSlug, a -> a)))
.build();
}
@DgsData(parentType = DgsConstants.QUERY_TYPE, field = QUERY.Articles)
public DataFetcherResult<ArticlesConnection> getArticles(
@InputArgument("first") Integer first,
@InputArgument("after") String after,
@InputArgument("last") Integer last,
@InputArgument("before") String before,
@InputArgument("authoredBy") String authoredBy,
@InputArgument("favoritedBy") String favoritedBy,
@InputArgument("withTag") String withTag,
DgsDataFetchingEnvironment dfe) {
if (first == null && last == null) {
throw new IllegalArgumentException("first 和 last 必须只存在一个");
}
User current = SecurityUtil.getCurrentUser().orElse(null);
CursorPager<ArticleData> articles;
if (first != null) {
articles =
articleQueryService.findRecentArticlesWithCursor(
withTag,
authoredBy,
favoritedBy,
new CursorPageParameter(after, first, Direction.NEXT),
current);
} else {
articles =
articleQueryService.findRecentArticlesWithCursor(
withTag,
authoredBy,
favoritedBy,
new CursorPageParameter(before, last, Direction.PREV),
current);
}
graphql.relay.PageInfo pageInfo = buildArticlePageInfo(articles);
ArticlesConnection articlesConnection =
ArticlesConnection.newBuilder()
.pageInfo(pageInfo)
.edges(
articles.getData().stream()
.map(
a ->
ArticleEdge.newBuilder()
.cursor(a.getCursor())
.node(buildArticleResult(a))
.build())
.collect(Collectors.toList()))
.build();
return DataFetcherResult.<ArticlesConnection>newResult()
.data(articlesConnection)
.localContext(
articles.getData().stream().collect(Collectors.toMap(ArticleData::getSlug, a -> a)))
.build();
}
@DgsData(parentType = ARTICLEPAYLOAD.TYPE_NAME, field = ARTICLEPAYLOAD.Article)
public DataFetcherResult<Article> getArticle(DataFetchingEnvironment dfe) {
io.spring.core.article.Article article = dfe.getLocalContext();
User current = SecurityUtil.getCurrentUser().orElse(null);
ArticleData articleData =
articleQueryService
.findById(article.getId(), current)
.orElseThrow(ResourceNotFoundException::new);
Article articleResult = buildArticleResult(articleData);
return DataFetcherResult.<Article>newResult()
.localContext(
new HashMap<String, Object>() {
{
put(articleData.getSlug(), articleData);
}
})
.data(articleResult)
.build();
}
@DgsData(parentType = COMMENT.TYPE_NAME, field = COMMENT.Article)
public DataFetcherResult<Article> getCommentArticle(
DataFetchingEnvironment dataFetchingEnvironment) {
CommentData comment = dataFetchingEnvironment.getLocalContext();
User current = SecurityUtil.getCurrentUser().orElse(null);
ArticleData articleData =
articleQueryService
.findById(comment.getArticleId(), current)
.orElseThrow(ResourceNotFoundException::new);
Article articleResult = buildArticleResult(articleData);
return DataFetcherResult.<Article>newResult()
.localContext(
new HashMap<String, Object>() {
{
put(articleData.getSlug(), articleData);
}
})
.data(articleResult)
.build();
}
@DgsData(parentType = DgsConstants.QUERY_TYPE, field = QUERY.Article)
public DataFetcherResult<Article> findArticleBySlug(@InputArgument("slug") String slug) {
User current = SecurityUtil.getCurrentUser().orElse(null);
ArticleData articleData =
articleQueryService.findBySlug(slug, current).orElseThrow(ResourceNotFoundException::new);
Article articleResult = buildArticleResult(articleData);
return DataFetcherResult.<Article>newResult()
.localContext(
new HashMap<String, Object>() {
{
put(articleData.getSlug(), articleData);
}
})
.data(articleResult)
.build();
}
private DefaultPageInfo buildArticlePageInfo(CursorPager<ArticleData> articles) {
return new DefaultPageInfo(
Util.isEmpty(articles.getStartCursor())
? null
: new DefaultConnectionCursor(articles.getStartCursor()),
Util.isEmpty(articles.getEndCursor())
? null
: new DefaultConnectionCursor(articles.getEndCursor()),
articles.hasPrevious(),
articles.hasNext());
}
private Article buildArticleResult(ArticleData articleData) {
return Article.newBuilder()
.body(articleData.getBody())
.createdAt(ISODateTimeFormat.dateTime().withZoneUTC().print(articleData.getCreatedAt()))
.description(articleData.getDescription())
.favorited(articleData.isFavorited())
.favoritesCount(articleData.getFavoritesCount())
.slug(articleData.getSlug())
.tagList(articleData.getTagList())
.title(articleData.getTitle())
.updatedAt(ISODateTimeFormat.dateTime().withZoneUTC().print(articleData.getUpdatedAt()))
.build();
}
}

View File

@ -0,0 +1,124 @@
package io.spring.graphql.users;
import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsData;
import com.netflix.graphql.dgs.InputArgument;
import graphql.execution.DataFetcherResult;
import io.spring.api.exception.NoAuthorizationException;
import io.spring.api.exception.ResourceNotFoundException;
import io.spring.application.article.ArticleCommandService;
import io.spring.application.article.NewArticleParam;
import io.spring.application.article.UpdateArticleParam;
import io.spring.core.article.Article;
import io.spring.core.article.ArticleRepository;
import io.spring.core.favorite.ArticleFavorite;
import io.spring.core.favorite.ArticleFavoriteRepository;
import io.spring.core.service.AuthorizationService;
import io.spring.core.user.User;
import io.spring.graphql.AuthenticationException;
import io.spring.graphql.DgsConstants.MUTATION;
import io.spring.graphql.types.ArticlePayload;
import io.spring.graphql.types.CreateArticleInput;
import io.spring.graphql.types.DeletionStatus;
import io.spring.graphql.types.UpdateArticleInput;
import java.util.Collections;
import org.springframework.beans.factory.annotation.Autowired;
@DgsComponent
public class ArticleMutation {
private ArticleCommandService articleCommandService;
private ArticleFavoriteRepository articleFavoriteRepository;
private ArticleRepository articleRepository;
@Autowired
public ArticleMutation(
ArticleCommandService articleCommandService,
ArticleFavoriteRepository articleFavoriteRepository,
ArticleRepository articleRepository) {
this.articleCommandService = articleCommandService;
this.articleFavoriteRepository = articleFavoriteRepository;
this.articleRepository = articleRepository;
}
@DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.CreateArticle)
public DataFetcherResult<ArticlePayload> createArticle(
@InputArgument("input") CreateArticleInput input) {
User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new);
NewArticleParam newArticleParam =
NewArticleParam.builder()
.title(input.getTitle())
.description(input.getDescription())
.body(input.getBody())
.tagList(input.getTagList() == null ? Collections.emptyList() : input.getTagList())
.build();
Article article = articleCommandService.createArticle(newArticleParam, user);
return DataFetcherResult.<ArticlePayload>newResult()
.data(ArticlePayload.newBuilder().build())
.localContext(article)
.build();
}
@DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.UpdateArticle)
public DataFetcherResult<ArticlePayload> updateArticle(
@InputArgument("slug") String slug, @InputArgument("changes") UpdateArticleInput params) {
Article article =
articleRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new);
User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new);
if (!AuthorizationService.canWriteArticle(user, article)) {
throw new NoAuthorizationException();
}
article =
articleCommandService.updateArticle(
article,
new UpdateArticleParam(params.getTitle(), params.getBody(), params.getDescription()));
return DataFetcherResult.<ArticlePayload>newResult()
.data(ArticlePayload.newBuilder().build())
.localContext(article)
.build();
}
@DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.FavoriteArticle)
public DataFetcherResult<ArticlePayload> favoriteArticle(@InputArgument("slug") String slug) {
User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new);
Article article =
articleRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new);
ArticleFavorite articleFavorite = new ArticleFavorite(article.getId(), user.getId());
articleFavoriteRepository.save(articleFavorite);
return DataFetcherResult.<ArticlePayload>newResult()
.data(ArticlePayload.newBuilder().build())
.localContext(article)
.build();
}
@DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.UnfavoriteArticle)
public DataFetcherResult<ArticlePayload> unfavoriteArticle(@InputArgument("slug") String slug) {
User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new);
Article article =
articleRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new);
articleFavoriteRepository
.find(article.getId(), user.getId())
.ifPresent(
favorite -> {
articleFavoriteRepository.remove(favorite);
});
return DataFetcherResult.<ArticlePayload>newResult()
.data(ArticlePayload.newBuilder().build())
.localContext(article)
.build();
}
@DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.DeleteArticle)
public DeletionStatus deleteArticle(@InputArgument("slug") String slug) {
User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new);
Article article =
articleRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new);
if (!AuthorizationService.canWriteArticle(user, article)) {
throw new NoAuthorizationException();
}
articleRepository.remove(article);
return DeletionStatus.newBuilder().success(true).build();
}
}

View File

@ -0,0 +1,118 @@
package io.spring.graphql.users;
import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsData;
import com.netflix.graphql.dgs.DgsDataFetchingEnvironment;
import com.netflix.graphql.dgs.InputArgument;
import graphql.execution.DataFetcherResult;
import graphql.relay.DefaultConnectionCursor;
import graphql.relay.DefaultPageInfo;
import io.spring.Util;
import io.spring.application.CommentQueryService;
import io.spring.application.CursorPageParameter;
import io.spring.application.CursorPager;
import io.spring.application.CursorPager.Direction;
import io.spring.application.data.ArticleData;
import io.spring.application.data.CommentData;
import io.spring.core.user.User;
import io.spring.graphql.DgsConstants.ARTICLE;
import io.spring.graphql.DgsConstants.COMMENTPAYLOAD;
import io.spring.graphql.types.Comment;
import io.spring.graphql.types.CommentEdge;
import io.spring.graphql.types.CommentsConnection;
import java.util.HashMap;
import java.util.stream.Collectors;
import org.joda.time.format.ISODateTimeFormat;
import org.springframework.beans.factory.annotation.Autowired;
@DgsComponent
public class CommentDatafetcher {
private CommentQueryService commentQueryService;
@Autowired
public CommentDatafetcher(CommentQueryService commentQueryService) {
this.commentQueryService = commentQueryService;
}
@DgsData(parentType = COMMENTPAYLOAD.TYPE_NAME, field = COMMENTPAYLOAD.Comment)
public DataFetcherResult<Comment> getComment(DgsDataFetchingEnvironment dfe) {
CommentData comment = dfe.getLocalContext();
Comment commentResult = buildCommentResult(comment);
return DataFetcherResult.<Comment>newResult()
.data(commentResult)
.localContext(
new HashMap<String, Object>() {
{
put(comment.getId(), comment);
}
})
.build();
}
@DgsData(parentType = ARTICLE.TYPE_NAME, field = ARTICLE.Comments)
public DataFetcherResult<CommentsConnection> articleComments(
@InputArgument("first") Integer first,
@InputArgument("after") String after,
@InputArgument("last") Integer last,
@InputArgument("before") String before,
DgsDataFetchingEnvironment dfe) {
if (first == null && last == null) {
throw new IllegalArgumentException("first 和 last 必须只存在一个");
}
User current = SecurityUtil.getCurrentUser().orElse(null);
ArticleData articleData = dfe.getLocalContext();
CursorPager<CommentData> comments;
if (first != null) {
comments =
commentQueryService.findByArticleIdWithCursor(
articleData.getId(), current, new CursorPageParameter(after, first, Direction.NEXT));
} else {
comments =
commentQueryService.findByArticleIdWithCursor(
articleData.getId(), current, new CursorPageParameter(before, last, Direction.PREV));
}
graphql.relay.PageInfo pageInfo = buildCommentPageInfo(comments);
CommentsConnection result =
CommentsConnection.newBuilder()
.pageInfo(pageInfo)
.edges(
comments.getData().stream()
.map(
a ->
CommentEdge.newBuilder()
.cursor(a.getCursor())
.node(buildCommentResult(a))
.build())
.collect(Collectors.toList()))
.build();
return DataFetcherResult.<CommentsConnection>newResult()
.data(result)
.localContext(
comments.getData().stream().collect(Collectors.toMap(CommentData::getId, c -> c)))
.build();
}
private DefaultPageInfo buildCommentPageInfo(CursorPager<CommentData> comments) {
return new DefaultPageInfo(
Util.isEmpty(comments.getStartCursor())
? null
: new DefaultConnectionCursor(comments.getStartCursor()),
Util.isEmpty(comments.getEndCursor())
? null
: new DefaultConnectionCursor(comments.getEndCursor()),
comments.hasPrevious(),
comments.hasNext());
}
private Comment buildCommentResult(CommentData comment) {
return Comment.newBuilder()
.id(comment.getId())
.body(comment.getBody())
.updatedAt(ISODateTimeFormat.dateTime().withZoneUTC().print(comment.getCreatedAt()))
.createdAt(ISODateTimeFormat.dateTime().withZoneUTC().print(comment.getCreatedAt()))
.build();
}
}

View File

@ -0,0 +1,77 @@
package io.spring.graphql.users;
import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsData;
import com.netflix.graphql.dgs.InputArgument;
import graphql.execution.DataFetcherResult;
import io.spring.api.exception.NoAuthorizationException;
import io.spring.api.exception.ResourceNotFoundException;
import io.spring.application.CommentQueryService;
import io.spring.application.data.CommentData;
import io.spring.core.article.Article;
import io.spring.core.article.ArticleRepository;
import io.spring.core.comment.Comment;
import io.spring.core.comment.CommentRepository;
import io.spring.core.service.AuthorizationService;
import io.spring.core.user.User;
import io.spring.graphql.AuthenticationException;
import io.spring.graphql.DgsConstants.MUTATION;
import io.spring.graphql.types.CommentPayload;
import io.spring.graphql.types.DeletionStatus;
import org.springframework.beans.factory.annotation.Autowired;
@DgsComponent
public class CommentMutation {
private ArticleRepository articleRepository;
private CommentRepository commentRepository;
private CommentQueryService commentQueryService;
@Autowired
public CommentMutation(
ArticleRepository articleRepository,
CommentRepository commentRepository,
CommentQueryService commentQueryService) {
this.articleRepository = articleRepository;
this.commentRepository = commentRepository;
this.commentQueryService = commentQueryService;
}
@DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.AddComment)
public DataFetcherResult<CommentPayload> createComment(
@InputArgument("slug") String slug, @InputArgument("body") String body) {
User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new);
Article article =
articleRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new);
Comment comment = new Comment(body, user.getId(), article.getId());
commentRepository.save(comment);
CommentData commentData =
commentQueryService
.findById(comment.getId(), user)
.orElseThrow(ResourceNotFoundException::new);
return DataFetcherResult.<CommentPayload>newResult()
.localContext(commentData)
.data(CommentPayload.newBuilder().build())
.build();
}
@DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.DeleteComment)
public DeletionStatus removeComment(
@InputArgument("slug") String slug, @InputArgument("id") String commentId) {
User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new);
Article article =
articleRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new);
return commentRepository
.findById(article.getId(), commentId)
.map(
comment -> {
if (!AuthorizationService.canWriteComment(user, article, comment)) {
throw new NoAuthorizationException();
}
commentRepository.remove(comment);
return DeletionStatus.newBuilder().success(true).build();
})
.orElseThrow(ResourceNotFoundException::new);
}
}

View File

@ -0,0 +1,67 @@
package io.spring.graphql.users;
import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsData;
import graphql.execution.DataFetcherResult;
import graphql.schema.DataFetchingEnvironment;
import io.spring.api.exception.ResourceNotFoundException;
import io.spring.application.UserQueryService;
import io.spring.application.data.UserData;
import io.spring.application.data.UserWithToken;
import io.spring.core.service.JwtService;
import io.spring.graphql.DgsConstants;
import io.spring.graphql.DgsConstants.QUERY;
import io.spring.graphql.DgsConstants.USERPAYLOAD;
import io.spring.graphql.types.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.RequestHeader;
@DgsComponent
public class MeDatafetcher {
private UserQueryService userQueryService;
private JwtService jwtService;
@Autowired
public MeDatafetcher(UserQueryService userQueryService, JwtService jwtService) {
this.userQueryService = userQueryService;
this.jwtService = jwtService;
}
@DgsData(parentType = DgsConstants.QUERY_TYPE, field = QUERY.Me)
public DataFetcherResult<User> getMe(
@RequestHeader(value = "Authorization") String authorization,
DataFetchingEnvironment dataFetchingEnvironment) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof AnonymousAuthenticationToken
|| authentication.getPrincipal() == null) {
return null;
}
io.spring.core.user.User user = (io.spring.core.user.User) authentication.getPrincipal();
UserData userData =
userQueryService.findById(user.getId()).orElseThrow(ResourceNotFoundException::new);
UserWithToken userWithToken = new UserWithToken(userData, authorization.split(" ")[1]);
User result =
User.newBuilder()
.email(userWithToken.getEmail())
.username(userWithToken.getUsername())
.token(userWithToken.getToken())
.build();
return DataFetcherResult.<User>newResult().data(result).localContext(user).build();
}
@DgsData(parentType = USERPAYLOAD.TYPE_NAME, field = USERPAYLOAD.User)
public DataFetcherResult<User> getUserPayloadUser(
DataFetchingEnvironment dataFetchingEnvironment) {
io.spring.core.user.User user = dataFetchingEnvironment.getLocalContext();
User result =
User.newBuilder()
.email(user.getEmail())
.username(user.getUsername())
.token(jwtService.toToken(user))
.build();
return DataFetcherResult.<User>newResult().data(result).localContext(user).build();
}
}

View File

@ -0,0 +1,76 @@
package io.spring.graphql.users;
import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsData;
import com.netflix.graphql.dgs.InputArgument;
import graphql.schema.DataFetchingEnvironment;
import io.spring.api.exception.ResourceNotFoundException;
import io.spring.application.ProfileQueryService;
import io.spring.application.data.ArticleData;
import io.spring.application.data.CommentData;
import io.spring.application.data.ProfileData;
import io.spring.core.user.User;
import io.spring.graphql.DgsConstants;
import io.spring.graphql.DgsConstants.ARTICLE;
import io.spring.graphql.DgsConstants.COMMENT;
import io.spring.graphql.DgsConstants.QUERY;
import io.spring.graphql.DgsConstants.USER;
import io.spring.graphql.types.Article;
import io.spring.graphql.types.Comment;
import io.spring.graphql.types.Profile;
import io.spring.graphql.types.ProfilePayload;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
@DgsComponent
public class ProfileDatafetcher {
private ProfileQueryService profileQueryService;
@Autowired
public ProfileDatafetcher(ProfileQueryService profileQueryService) {
this.profileQueryService = profileQueryService;
}
@DgsData(parentType = USER.TYPE_NAME, field = USER.Profile)
public Profile getUserProfile(DataFetchingEnvironment dataFetchingEnvironment) {
User user = dataFetchingEnvironment.getLocalContext();
String username = user.getUsername();
return queryProfile(username);
}
@DgsData(parentType = ARTICLE.TYPE_NAME, field = ARTICLE.Author)
public Profile getAuthor(DataFetchingEnvironment dataFetchingEnvironment) {
Map<String, ArticleData> map = dataFetchingEnvironment.getLocalContext();
Article article = dataFetchingEnvironment.getSource();
return queryProfile(map.get(article.getSlug()).getProfileData().getUsername());
}
@DgsData(parentType = COMMENT.TYPE_NAME, field = COMMENT.Author)
public Profile getCommentAuthor(DataFetchingEnvironment dataFetchingEnvironment) {
Comment comment = dataFetchingEnvironment.getSource();
Map<String, CommentData> map = dataFetchingEnvironment.getLocalContext();
return queryProfile(map.get(comment.getId()).getProfileData().getUsername());
}
@DgsData(parentType = DgsConstants.QUERY_TYPE, field = QUERY.Profile)
public ProfilePayload queryProfile(
@InputArgument("username") String username, DataFetchingEnvironment dataFetchingEnvironment) {
Profile profile = queryProfile(dataFetchingEnvironment.getArgument("username"));
return ProfilePayload.newBuilder().profile(profile).build();
}
private Profile queryProfile(String username) {
User current = SecurityUtil.getCurrentUser().orElse(null);
ProfileData profileData =
profileQueryService
.findByUsername(username, current)
.orElseThrow(ResourceNotFoundException::new);
return Profile.newBuilder()
.username(profileData.getUsername())
.bio(profileData.getBio())
.image(profileData.getImage())
.following(profileData.isFollowing())
.build();
}
}

View File

@ -0,0 +1,70 @@
package io.spring.graphql.users;
import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsData;
import com.netflix.graphql.dgs.InputArgument;
import io.spring.api.exception.ResourceNotFoundException;
import io.spring.application.ProfileQueryService;
import io.spring.application.data.ProfileData;
import io.spring.core.user.FollowRelation;
import io.spring.core.user.User;
import io.spring.core.user.UserRepository;
import io.spring.graphql.AuthenticationException;
import io.spring.graphql.DgsConstants.MUTATION;
import io.spring.graphql.types.Profile;
import io.spring.graphql.types.ProfilePayload;
import org.springframework.beans.factory.annotation.Autowired;
@DgsComponent
public class RelationMutation {
private UserRepository userRepository;
private ProfileQueryService profileQueryService;
@Autowired
public RelationMutation(UserRepository userRepository, ProfileQueryService profileQueryService) {
this.userRepository = userRepository;
this.profileQueryService = profileQueryService;
}
@DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.FollowUser)
public ProfilePayload follow(@InputArgument("username") String username) {
User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new);
return userRepository
.findByUsername(username)
.map(
target -> {
FollowRelation followRelation = new FollowRelation(user.getId(), target.getId());
userRepository.saveRelation(followRelation);
Profile profile = buildProfile(username, user);
return ProfilePayload.newBuilder().profile(profile).build();
})
.orElseThrow(ResourceNotFoundException::new);
}
@DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.UnfollowUser)
public ProfilePayload unfollow(@InputArgument("username") String username) {
User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new);
User target =
userRepository.findByUsername(username).orElseThrow(ResourceNotFoundException::new);
return userRepository
.findRelation(user.getId(), target.getId())
.map(
relation -> {
userRepository.removeRelation(relation);
Profile profile = buildProfile(username, user);
return ProfilePayload.newBuilder().profile(profile).build();
})
.orElseThrow(ResourceNotFoundException::new);
}
private Profile buildProfile(@InputArgument("username") String username, User current) {
ProfileData profileData = profileQueryService.findByUsername(username, current).get();
return Profile.newBuilder()
.username(profileData.getUsername())
.bio(profileData.getBio())
.image(profileData.getImage())
.following(profileData.isFollowing())
.build();
}
}

View File

@ -0,0 +1,19 @@
package io.spring.graphql.users;
import io.spring.core.user.User;
import java.util.Optional;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
public class SecurityUtil {
public static Optional<User> getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof AnonymousAuthenticationToken
|| authentication.getPrincipal() == null) {
return Optional.empty();
}
io.spring.core.user.User currentUser = (io.spring.core.user.User) authentication.getPrincipal();
return Optional.of(currentUser);
}
}

View File

@ -0,0 +1,90 @@
package io.spring.graphql.users;
import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsData;
import com.netflix.graphql.dgs.InputArgument;
import graphql.execution.DataFetcherResult;
import io.spring.api.exception.InvalidAuthenticationException;
import io.spring.application.user.RegisterParam;
import io.spring.application.user.UpdateUserCommand;
import io.spring.application.user.UpdateUserParam;
import io.spring.application.user.UserService;
import io.spring.core.user.EncryptService;
import io.spring.core.user.User;
import io.spring.core.user.UserRepository;
import io.spring.graphql.DgsConstants.MUTATION;
import io.spring.graphql.types.CreateUserInput;
import io.spring.graphql.types.UpdateUserInput;
import io.spring.graphql.types.UserPayload;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
@DgsComponent
public class UserMutation {
private UserRepository userRepository;
private EncryptService encryptService;
private UserService userService;
@Autowired
public UserMutation(
UserRepository userRepository, EncryptService encryptService, UserService userService) {
this.userRepository = userRepository;
this.encryptService = encryptService;
this.userService = userService;
}
@DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.CreateUser)
public DataFetcherResult<UserPayload> createUser(@InputArgument("input") CreateUserInput input) {
RegisterParam registerParam =
new RegisterParam(input.getEmail(), input.getUsername(), input.getPassword());
User user = userService.createUser(registerParam);
return DataFetcherResult.<UserPayload>newResult()
.data(UserPayload.newBuilder().build())
.localContext(user)
.build();
}
@DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.Login)
public DataFetcherResult<UserPayload> login(
@InputArgument("password") String password, @InputArgument("email") String email) {
Optional<User> optional = userRepository.findByEmail(email);
if (optional.isPresent() && encryptService.check(password, optional.get().getPassword())) {
return DataFetcherResult.<UserPayload>newResult()
.data(UserPayload.newBuilder().build())
.localContext(optional.get())
.build();
} else {
throw new InvalidAuthenticationException();
}
}
@DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.UpdateUser)
public DataFetcherResult<UserPayload> updateUser(
@InputArgument("changes") UpdateUserInput updateUserInput) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof AnonymousAuthenticationToken
|| authentication.getPrincipal() == null) {
return null;
}
io.spring.core.user.User currentUser = (io.spring.core.user.User) authentication.getPrincipal();
UpdateUserParam param =
UpdateUserParam.builder()
.username(updateUserInput.getUsername())
.email(updateUserInput.getEmail())
.bio(updateUserInput.getBio())
.password(updateUserInput.getPassword())
.image(updateUserInput.getImage())
.build();
userService.updateUser(new UpdateUserCommand(currentUser, param));
return DataFetcherResult.<UserPayload>newResult()
.data(UserPayload.newBuilder().build())
.localContext(currentUser)
.build();
}
}

View File

@ -0,0 +1,164 @@
# Build the schema.
type Query {
article(slug: String!): Article
articles(
first: Int,
after: String,
last: Int,
before: String,
authoredBy: String
favoritedBy: String
withTag: String
): ArticlesConnection
me: User
feed(first: Int, after: String, last: Int, before: String): ArticlesConnection
profile(username: String!): ProfilePayload
tags: [String]
}
type Mutation {
### User & Profile
createUser(input: CreateUserInput): UserPayload
login(password: String!, email: String!): UserPayload
updateUser(changes: UpdateUserInput!): UserPayload
followUser(username: String!): ProfilePayload
unfollowUser(username: String!): ProfilePayload
### Article
createArticle(input: CreateArticleInput!): ArticlePayload
updateArticle(slug: String!, changes: UpdateArticleInput!): ArticlePayload
favoriteArticle(slug: String!): ArticlePayload
unfavoriteArticle(slug: String!): ArticlePayload
deleteArticle(slug: String!): DeletionStatus
### Comment
addComment(slug: String!, body: String!): CommentPayload
deleteComment(slug: String!, id: ID!): DeletionStatus
}
schema {
query: Query
mutation: Mutation
}
### Articles
type Article {
author: Profile!
body: String!
comments(first: Int, after: String, last: Int, before: String): CommentsConnection
createdAt: String!
description: String!
favorited: Boolean!
favoritesCount: Int!
slug: String!
tagList: [String],
title: String!
updatedAt: String!
}
type ArticleEdge {
cursor: String!
node: Article
}
type ArticlesConnection {
edges: [ArticleEdge]
pageInfo: PageInfo!
}
### Comments
type Comment {
id: ID!
author: Profile!
article: Article!
body: String!
createdAt: String!
updatedAt: String!
}
type CommentEdge {
cursor: String!
node: Comment
}
type CommentsConnection {
edges: [CommentEdge]
pageInfo: PageInfo!
}
type DeletionStatus {
success: Boolean!
}
type PageInfo {
endCursor: String
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
}
### Profile
type Profile {
username: String!
bio: String
following: Boolean!
image: String
articles(first: Int, after: String, last: Int, before: String): ArticlesConnection
favorites(first: Int, after: String, last: Int, before: String): ArticlesConnection
feed(first: Int, after: String, last: Int, before: String): ArticlesConnection
}
### User
type User {
email: String!
profile: Profile!
token: String!
username: String!
}
## Mutations
# Input types.
input UpdateArticleInput {
body: String
description: String
title: String
}
input CreateArticleInput {
body: String!
description: String!
tagList: [String]
title: String!
}
type ArticlePayload {
article: Article
}
type CommentPayload {
comment: Comment
}
input CreateUserInput {
email: String!
username: String!
password: String!
}
input UpdateUserInput {
email: String
username: String
password: String
image: String
bio: String
}
type UserPayload {
user: User
}
type ProfilePayload {
profile: Profile
}