feat: add graphql
This commit is contained in:
parent
01fac42c64
commit
44a70270c8
@ -0,0 +1,3 @@
|
|||||||
|
package io.spring.graphql;
|
||||||
|
|
||||||
|
public class AuthenticationException extends RuntimeException {}
|
386
src/main/java/io/spring/graphql/users/ArticleDatafetcher.java
Normal file
386
src/main/java/io/spring/graphql/users/ArticleDatafetcher.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
124
src/main/java/io/spring/graphql/users/ArticleMutation.java
Normal file
124
src/main/java/io/spring/graphql/users/ArticleMutation.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
118
src/main/java/io/spring/graphql/users/CommentDatafetcher.java
Normal file
118
src/main/java/io/spring/graphql/users/CommentDatafetcher.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
77
src/main/java/io/spring/graphql/users/CommentMutation.java
Normal file
77
src/main/java/io/spring/graphql/users/CommentMutation.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
67
src/main/java/io/spring/graphql/users/MeDatafetcher.java
Normal file
67
src/main/java/io/spring/graphql/users/MeDatafetcher.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
70
src/main/java/io/spring/graphql/users/RelationMutation.java
Normal file
70
src/main/java/io/spring/graphql/users/RelationMutation.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
19
src/main/java/io/spring/graphql/users/SecurityUtil.java
Normal file
19
src/main/java/io/spring/graphql/users/SecurityUtil.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
90
src/main/java/io/spring/graphql/users/UserMutation.java
Normal file
90
src/main/java/io/spring/graphql/users/UserMutation.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
164
src/main/resources/schema/schema.graphqls
Normal file
164
src/main/resources/schema/schema.graphqls
Normal 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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user