diff --git a/.framework/java/backend/build.gradle b/.framework/java/backend/build.gradle index c034e0f9..e1db9a18 100644 --- a/.framework/java/backend/build.gradle +++ b/.framework/java/backend/build.gradle @@ -2,7 +2,6 @@ plugins { id 'org.springframework.boot' version '2.6.3' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' - id "com.netflix.dgs.codegen" version "5.0.6" id "com.diffplug.spotless" version "6.2.1" } @@ -39,7 +38,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-devtools' implementation 'com.squareup.okhttp3:okhttp:4.9.1' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2' - implementation 'com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter:4.9.21' implementation 'org.flywaydb:flyway-core' implementation 'io.jsonwebtoken:jjwt-api:0.11.2' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2', @@ -71,8 +69,3 @@ tasks.named('clean') { delete './dev.db' } } - -tasks.named('generateJava') { - schemaPaths = ["${projectDir}/src/main/resources/schema"] // List of directories containing schema files - packageName = 'io.spring.graphql' // The package name to use to generate sources -} diff --git a/.framework/java/backend/src/main/java/io/spring/api/security/WebSecurityConfig.java b/.framework/java/backend/src/main/java/io/spring/api/security/WebSecurityConfig.java index 94b3ae40..b1f463d5 100644 --- a/.framework/java/backend/src/main/java/io/spring/api/security/WebSecurityConfig.java +++ b/.framework/java/backend/src/main/java/io/spring/api/security/WebSecurityConfig.java @@ -48,16 +48,12 @@ protected void configure(HttpSecurity http) throws Exception { .authorizeRequests() .antMatchers(HttpMethod.OPTIONS) .permitAll() - .antMatchers("/graphiql") - .permitAll() .antMatchers("/actuator/**") .permitAll() .antMatchers("/health") .permitAll() .antMatchers("/api/ping") .permitAll() - .antMatchers("/graphql") - .permitAll() .antMatchers(HttpMethod.GET, "/api/items/feed") .authenticated() .antMatchers(HttpMethod.POST, "/api/users", "/api/users/login") diff --git a/.framework/java/backend/src/main/java/io/spring/graphql/CommentDatafetcher.java b/.framework/java/backend/src/main/java/io/spring/graphql/CommentDatafetcher.java deleted file mode 100644 index 4f4bc896..00000000 --- a/.framework/java/backend/src/main/java/io/spring/graphql/CommentDatafetcher.java +++ /dev/null @@ -1,122 +0,0 @@ -package io.spring.graphql; - -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.application.CommentQueryService; -import io.spring.application.CursorPageParameter; -import io.spring.application.CursorPager; -import io.spring.application.CursorPager.Direction; -import io.spring.application.DateTimeCursor; -import io.spring.application.data.CommentData; -import io.spring.application.data.ItemData; -import io.spring.core.user.User; -import io.spring.graphql.DgsConstants.COMMENTPAYLOAD; -import io.spring.graphql.DgsConstants.ITEM; -import io.spring.graphql.types.Comment; -import io.spring.graphql.types.CommentEdge; -import io.spring.graphql.types.CommentsConnection; -import io.spring.graphql.types.Item; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; -import lombok.AllArgsConstructor; -import org.joda.time.format.ISODateTimeFormat; - -@DgsComponent -@AllArgsConstructor -public class CommentDatafetcher { - private CommentQueryService commentQueryService; - - @DgsData(parentType = COMMENTPAYLOAD.TYPE_NAME, field = COMMENTPAYLOAD.Comment) - public DataFetcherResult getComment(DgsDataFetchingEnvironment dfe) { - CommentData comment = dfe.getLocalContext(); - Comment commentResult = buildCommentResult(comment); - return DataFetcherResult.newResult() - .data(commentResult) - .localContext( - new HashMap() { - { - put(comment.getId(), comment); - } - }) - .build(); - } - - @DgsData(parentType = ITEM.TYPE_NAME, field = ITEM.Comments) - public DataFetcherResult itemComments( - @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); - Item item = dfe.getSource(); - Map map = dfe.getLocalContext(); - ItemData itemData = map.get(item.getSlug()); - - CursorPager comments; - if (first != null) { - comments = - commentQueryService.findByItemIdWithCursor( - itemData.getId(), - current, - new CursorPageParameter<>(DateTimeCursor.parse(after), first, Direction.NEXT)); - } else { - comments = - commentQueryService.findByItemIdWithCursor( - itemData.getId(), - current, - new CursorPageParameter<>(DateTimeCursor.parse(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().toString()) - .node(buildCommentResult(a)) - .build()) - .collect(Collectors.toList())) - .build(); - return DataFetcherResult.newResult() - .data(result) - .localContext( - comments.getData().stream().collect(Collectors.toMap(CommentData::getId, c -> c))) - .build(); - } - - private DefaultPageInfo buildCommentPageInfo(CursorPager comments) { - return new DefaultPageInfo( - comments.getStartCursor() == null - ? null - : new DefaultConnectionCursor(comments.getStartCursor().toString()), - comments.getEndCursor() == null - ? null - : new DefaultConnectionCursor(comments.getEndCursor().toString()), - 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(); - } -} diff --git a/.framework/java/backend/src/main/java/io/spring/graphql/CommentMutation.java b/.framework/java/backend/src/main/java/io/spring/graphql/CommentMutation.java deleted file mode 100644 index 35b7f761..00000000 --- a/.framework/java/backend/src/main/java/io/spring/graphql/CommentMutation.java +++ /dev/null @@ -1,66 +0,0 @@ -package io.spring.graphql; - -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.comment.Comment; -import io.spring.core.comment.CommentRepository; -import io.spring.core.item.Item; -import io.spring.core.item.ItemRepository; -import io.spring.core.service.AuthorizationService; -import io.spring.core.user.User; -import io.spring.graphql.DgsConstants.MUTATION; -import io.spring.graphql.exception.AuthenticationException; -import io.spring.graphql.types.CommentPayload; -import io.spring.graphql.types.DeletionStatus; -import lombok.AllArgsConstructor; - -@DgsComponent -@AllArgsConstructor -public class CommentMutation { - - private ItemRepository itemRepository; - private CommentRepository commentRepository; - private CommentQueryService commentQueryService; - - @DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.AddComment) - public DataFetcherResult createComment( - @InputArgument("slug") String slug, @InputArgument("body") String body) { - User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new); - Item item = itemRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new); - Comment comment = new Comment(body, user.getId(), item.getId()); - commentRepository.save(comment); - CommentData commentData = - commentQueryService - .findById(comment.getId(), user) - .orElseThrow(ResourceNotFoundException::new); - return DataFetcherResult.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); - - Item item = itemRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new); - return commentRepository - .findById(item.getId(), commentId) - .map( - comment -> { - if (!AuthorizationService.canWriteComment(user, item, comment)) { - throw new NoAuthorizationException(); - } - commentRepository.remove(comment); - return DeletionStatus.newBuilder().success(true).build(); - }) - .orElseThrow(ResourceNotFoundException::new); - } -} diff --git a/.framework/java/backend/src/main/java/io/spring/graphql/ItemDatafetcher.java b/.framework/java/backend/src/main/java/io/spring/graphql/ItemDatafetcher.java deleted file mode 100644 index 3dd97d65..00000000 --- a/.framework/java/backend/src/main/java/io/spring/graphql/ItemDatafetcher.java +++ /dev/null @@ -1,378 +0,0 @@ -package io.spring.graphql; - -import com.netflix.graphql.dgs.DgsComponent; -import com.netflix.graphql.dgs.DgsData; -import com.netflix.graphql.dgs.DgsDataFetchingEnvironment; -import com.netflix.graphql.dgs.DgsQuery; -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.api.exception.ResourceNotFoundException; -import io.spring.application.CursorPageParameter; -import io.spring.application.CursorPager; -import io.spring.application.CursorPager.Direction; -import io.spring.application.DateTimeCursor; -import io.spring.application.ItemQueryService; -import io.spring.application.data.CommentData; -import io.spring.application.data.ItemData; -import io.spring.core.user.User; -import io.spring.core.user.UserRepository; -import io.spring.graphql.DgsConstants.COMMENT; -import io.spring.graphql.DgsConstants.ITEMPAYLOAD; -import io.spring.graphql.DgsConstants.PROFILE; -import io.spring.graphql.DgsConstants.QUERY; -import io.spring.graphql.types.Item; -import io.spring.graphql.types.ItemEdge; -import io.spring.graphql.types.ItemsConnection; -import io.spring.graphql.types.Profile; -import java.util.HashMap; -import java.util.stream.Collectors; -import lombok.AllArgsConstructor; -import org.joda.time.format.ISODateTimeFormat; - -@DgsComponent -@AllArgsConstructor -public class ItemDatafetcher { - - private ItemQueryService itemQueryService; - private UserRepository userRepository; - - @DgsQuery(field = QUERY.Feed) - public DataFetcherResult 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 items; - if (first != null) { - items = - itemQueryService.findUserFeedWithCursor( - current, - new CursorPageParameter<>(DateTimeCursor.parse(after), first, Direction.NEXT)); - } else { - items = - itemQueryService.findUserFeedWithCursor( - current, - new CursorPageParameter<>(DateTimeCursor.parse(before), last, Direction.PREV)); - } - graphql.relay.PageInfo pageInfo = buildItemPageInfo(items); - ItemsConnection itemsConnection = - ItemsConnection.newBuilder() - .pageInfo(pageInfo) - .edges( - items.getData().stream() - .map( - a -> - ItemEdge.newBuilder() - .cursor(a.getCursor().toString()) - .node(buildItemResult(a)) - .build()) - .collect(Collectors.toList())) - .build(); - return DataFetcherResult.newResult() - .data(itemsConnection) - .localContext(items.getData().stream().collect(Collectors.toMap(ItemData::getSlug, a -> a))) - .build(); - } - - @DgsData(parentType = PROFILE.TYPE_NAME, field = PROFILE.Feed) - public DataFetcherResult 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 items; - if (first != null) { - items = - itemQueryService.findUserFeedWithCursor( - target, - new CursorPageParameter<>(DateTimeCursor.parse(after), first, Direction.NEXT)); - } else { - items = - itemQueryService.findUserFeedWithCursor( - target, - new CursorPageParameter<>(DateTimeCursor.parse(before), last, Direction.PREV)); - } - graphql.relay.PageInfo pageInfo = buildItemPageInfo(items); - ItemsConnection itemsConnection = - ItemsConnection.newBuilder() - .pageInfo(pageInfo) - .edges( - items.getData().stream() - .map( - a -> - ItemEdge.newBuilder() - .cursor(a.getCursor().toString()) - .node(buildItemResult(a)) - .build()) - .collect(Collectors.toList())) - .build(); - return DataFetcherResult.newResult() - .data(itemsConnection) - .localContext(items.getData().stream().collect(Collectors.toMap(ItemData::getSlug, a -> a))) - .build(); - } - - @DgsData(parentType = PROFILE.TYPE_NAME, field = PROFILE.Favorites) - public DataFetcherResult 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 items; - if (first != null) { - items = - itemQueryService.findRecentItemsWithCursor( - null, - null, - profile.getUsername(), - new CursorPageParameter<>(DateTimeCursor.parse(after), first, Direction.NEXT), - current); - } else { - items = - itemQueryService.findRecentItemsWithCursor( - null, - null, - profile.getUsername(), - new CursorPageParameter<>(DateTimeCursor.parse(before), last, Direction.PREV), - current); - } - graphql.relay.PageInfo pageInfo = buildItemPageInfo(items); - - ItemsConnection itemsConnection = - ItemsConnection.newBuilder() - .pageInfo(pageInfo) - .edges( - items.getData().stream() - .map( - a -> - ItemEdge.newBuilder() - .cursor(a.getCursor().toString()) - .node(buildItemResult(a)) - .build()) - .collect(Collectors.toList())) - .build(); - return DataFetcherResult.newResult() - .data(itemsConnection) - .localContext(items.getData().stream().collect(Collectors.toMap(ItemData::getSlug, a -> a))) - .build(); - } - - @DgsData(parentType = PROFILE.TYPE_NAME, field = PROFILE.Items) - public DataFetcherResult userItems( - @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 items; - if (first != null) { - items = - itemQueryService.findRecentItemsWithCursor( - null, - profile.getUsername(), - null, - new CursorPageParameter<>(DateTimeCursor.parse(after), first, Direction.NEXT), - current); - } else { - items = - itemQueryService.findRecentItemsWithCursor( - null, - profile.getUsername(), - null, - new CursorPageParameter<>(DateTimeCursor.parse(before), last, Direction.PREV), - current); - } - graphql.relay.PageInfo pageInfo = buildItemPageInfo(items); - ItemsConnection itemsConnection = - ItemsConnection.newBuilder() - .pageInfo(pageInfo) - .edges( - items.getData().stream() - .map( - a -> - ItemEdge.newBuilder() - .cursor(a.getCursor().toString()) - .node(buildItemResult(a)) - .build()) - .collect(Collectors.toList())) - .build(); - return DataFetcherResult.newResult() - .data(itemsConnection) - .localContext(items.getData().stream().collect(Collectors.toMap(ItemData::getSlug, a -> a))) - .build(); - } - - @DgsData(parentType = DgsConstants.QUERY_TYPE, field = QUERY.Items) - public DataFetcherResult getItems( - @InputArgument("first") Integer first, - @InputArgument("after") String after, - @InputArgument("last") Integer last, - @InputArgument("before") String before, - @InputArgument("soldBy") String soldBy, - @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 items; - if (first != null) { - items = - itemQueryService.findRecentItemsWithCursor( - withTag, - soldBy, - favoritedBy, - new CursorPageParameter<>(DateTimeCursor.parse(after), first, Direction.NEXT), - current); - } else { - items = - itemQueryService.findRecentItemsWithCursor( - withTag, - soldBy, - favoritedBy, - new CursorPageParameter<>(DateTimeCursor.parse(before), last, Direction.PREV), - current); - } - graphql.relay.PageInfo pageInfo = buildItemPageInfo(items); - ItemsConnection itemsConnection = - ItemsConnection.newBuilder() - .pageInfo(pageInfo) - .edges( - items.getData().stream() - .map( - a -> - ItemEdge.newBuilder() - .cursor(a.getCursor().toString()) - .node(buildItemResult(a)) - .build()) - .collect(Collectors.toList())) - .build(); - return DataFetcherResult.newResult() - .data(itemsConnection) - .localContext(items.getData().stream().collect(Collectors.toMap(ItemData::getSlug, a -> a))) - .build(); - } - - @DgsData(parentType = ITEMPAYLOAD.TYPE_NAME, field = ITEMPAYLOAD.Item) - public DataFetcherResult getItem(DataFetchingEnvironment dfe) { - io.spring.core.item.Item item = dfe.getLocalContext(); - - User current = SecurityUtil.getCurrentUser().orElse(null); - ItemData itemData = - itemQueryService - .findById(item.getId(), current) - .orElseThrow(ResourceNotFoundException::new); - Item itemResult = buildItemResult(itemData); - return DataFetcherResult.newResult() - .localContext( - new HashMap() { - { - put(itemData.getSlug(), itemData); - } - }) - .data(itemResult) - .build(); - } - - @DgsData(parentType = COMMENT.TYPE_NAME, field = COMMENT.Item) - public DataFetcherResult getCommentItem(DataFetchingEnvironment dataFetchingEnvironment) { - CommentData comment = dataFetchingEnvironment.getLocalContext(); - User current = SecurityUtil.getCurrentUser().orElse(null); - ItemData itemData = - itemQueryService - .findById(comment.getItemId(), current) - .orElseThrow(ResourceNotFoundException::new); - Item itemResult = buildItemResult(itemData); - return DataFetcherResult.newResult() - .localContext( - new HashMap() { - { - put(itemData.getSlug(), itemData); - } - }) - .data(itemResult) - .build(); - } - - @DgsQuery(field = QUERY.Item) - public DataFetcherResult findItemBySlug(@InputArgument("slug") String slug) { - User current = SecurityUtil.getCurrentUser().orElse(null); - ItemData itemData = - itemQueryService.findBySlug(slug, current).orElseThrow(ResourceNotFoundException::new); - Item itemResult = buildItemResult(itemData); - return DataFetcherResult.newResult() - .localContext( - new HashMap() { - { - put(itemData.getSlug(), itemData); - } - }) - .data(itemResult) - .build(); - } - - private DefaultPageInfo buildItemPageInfo(CursorPager items) { - return new DefaultPageInfo( - items.getStartCursor() == null - ? null - : new DefaultConnectionCursor(items.getStartCursor().toString()), - items.getEndCursor() == null - ? null - : new DefaultConnectionCursor(items.getEndCursor().toString()), - items.hasPrevious(), - items.hasNext()); - } - - private Item buildItemResult(ItemData itemData) { - return Item.newBuilder() - .image(itemData.getImage()) - .createdAt(ISODateTimeFormat.dateTime().withZoneUTC().print(itemData.getCreatedAt())) - .description(itemData.getDescription()) - .favorited(itemData.isFavorited()) - .favoritesCount(itemData.getFavoritesCount()) - .slug(itemData.getSlug()) - .tagList(itemData.getTagList()) - .title(itemData.getTitle()) - .updatedAt(ISODateTimeFormat.dateTime().withZoneUTC().print(itemData.getUpdatedAt())) - .build(); - } -} diff --git a/.framework/java/backend/src/main/java/io/spring/graphql/ItemMutation.java b/.framework/java/backend/src/main/java/io/spring/graphql/ItemMutation.java deleted file mode 100644 index 2e09d92f..00000000 --- a/.framework/java/backend/src/main/java/io/spring/graphql/ItemMutation.java +++ /dev/null @@ -1,110 +0,0 @@ -package io.spring.graphql; - -import com.netflix.graphql.dgs.DgsComponent; -import com.netflix.graphql.dgs.DgsMutation; -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.item.ItemCommandService; -import io.spring.application.item.NewItemParam; -import io.spring.application.item.UpdateItemParam; -import io.spring.core.favorite.ItemFavorite; -import io.spring.core.favorite.ItemFavoriteRepository; -import io.spring.core.item.Item; -import io.spring.core.item.ItemRepository; -import io.spring.core.service.AuthorizationService; -import io.spring.core.user.User; -import io.spring.graphql.DgsConstants.MUTATION; -import io.spring.graphql.exception.AuthenticationException; -import io.spring.graphql.types.CreateItemInput; -import io.spring.graphql.types.DeletionStatus; -import io.spring.graphql.types.ItemPayload; -import io.spring.graphql.types.UpdateItemInput; -import java.util.Collections; -import lombok.AllArgsConstructor; - -@DgsComponent -@AllArgsConstructor -public class ItemMutation { - - private ItemCommandService itemCommandService; - private ItemFavoriteRepository itemFavoriteRepository; - private ItemRepository itemRepository; - - @DgsMutation(field = MUTATION.CreateItem) - public DataFetcherResult createItem(@InputArgument("input") CreateItemInput input) { - User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new); - NewItemParam newItemParam = - NewItemParam.builder() - .title(input.getTitle()) - .description(input.getDescription()) - .image(input.getImage()) - .tagList(input.getTagList() == null ? Collections.emptyList() : input.getTagList()) - .build(); - Item item = itemCommandService.createItem(newItemParam, user); - return DataFetcherResult.newResult() - .data(ItemPayload.newBuilder().build()) - .localContext(item) - .build(); - } - - @DgsMutation(field = MUTATION.UpdateItem) - public DataFetcherResult updateItem( - @InputArgument("slug") String slug, @InputArgument("changes") UpdateItemInput params) { - Item item = itemRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new); - User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new); - if (!AuthorizationService.canWriteItem(user, item)) { - throw new NoAuthorizationException(); - } - item = - itemCommandService.updateItem( - item, - new UpdateItemParam(params.getTitle(), params.getImage(), params.getDescription())); - return DataFetcherResult.newResult() - .data(ItemPayload.newBuilder().build()) - .localContext(item) - .build(); - } - - @DgsMutation(field = MUTATION.FavoriteItem) - public DataFetcherResult favoriteItem(@InputArgument("slug") String slug) { - User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new); - Item item = itemRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new); - ItemFavorite itemFavorite = new ItemFavorite(item.getId(), user.getId()); - itemFavoriteRepository.save(itemFavorite); - return DataFetcherResult.newResult() - .data(ItemPayload.newBuilder().build()) - .localContext(item) - .build(); - } - - @DgsMutation(field = MUTATION.UnfavoriteItem) - public DataFetcherResult unfavoriteItem(@InputArgument("slug") String slug) { - User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new); - Item item = itemRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new); - itemFavoriteRepository - .find(item.getId(), user.getId()) - .ifPresent( - favorite -> { - itemFavoriteRepository.remove(favorite); - }); - return DataFetcherResult.newResult() - .data(ItemPayload.newBuilder().build()) - .localContext(item) - .build(); - } - - @DgsMutation(field = MUTATION.DeleteItem) - public DeletionStatus deleteItem(@InputArgument("slug") String slug) { - User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new); - Item item = itemRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new); - - if (!AuthorizationService.canWriteItem(user, item)) { - throw new NoAuthorizationException(); - } - - itemRepository.remove(item); - return DeletionStatus.newBuilder().success(true).build(); - } -} diff --git a/.framework/java/backend/src/main/java/io/spring/graphql/MeDatafetcher.java b/.framework/java/backend/src/main/java/io/spring/graphql/MeDatafetcher.java deleted file mode 100644 index 93985967..00000000 --- a/.framework/java/backend/src/main/java/io/spring/graphql/MeDatafetcher.java +++ /dev/null @@ -1,61 +0,0 @@ -package io.spring.graphql; - -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.QUERY; -import io.spring.graphql.DgsConstants.USERPAYLOAD; -import io.spring.graphql.types.User; -import lombok.AllArgsConstructor; -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 -@AllArgsConstructor -public class MeDatafetcher { - private UserQueryService userQueryService; - private JwtService jwtService; - - @DgsData(parentType = DgsConstants.QUERY_TYPE, field = QUERY.Me) - public DataFetcherResult 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.newResult().data(result).localContext(user).build(); - } - - @DgsData(parentType = USERPAYLOAD.TYPE_NAME, field = USERPAYLOAD.User) - public DataFetcherResult 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.newResult().data(result).localContext(user).build(); - } -} diff --git a/.framework/java/backend/src/main/java/io/spring/graphql/ProfileDatafetcher.java b/.framework/java/backend/src/main/java/io/spring/graphql/ProfileDatafetcher.java deleted file mode 100644 index e421eea5..00000000 --- a/.framework/java/backend/src/main/java/io/spring/graphql/ProfileDatafetcher.java +++ /dev/null @@ -1,71 +0,0 @@ -package io.spring.graphql; - -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.CommentData; -import io.spring.application.data.ItemData; -import io.spring.application.data.ProfileData; -import io.spring.core.user.User; -import io.spring.graphql.DgsConstants.COMMENT; -import io.spring.graphql.DgsConstants.ITEM; -import io.spring.graphql.DgsConstants.QUERY; -import io.spring.graphql.DgsConstants.USER; -import io.spring.graphql.types.Comment; -import io.spring.graphql.types.Item; -import io.spring.graphql.types.Profile; -import io.spring.graphql.types.ProfilePayload; -import java.util.Map; -import lombok.AllArgsConstructor; - -@DgsComponent -@AllArgsConstructor -public class ProfileDatafetcher { - - private 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 = ITEM.TYPE_NAME, field = ITEM.Seller) - public Profile getSeller(DataFetchingEnvironment dataFetchingEnvironment) { - Map map = dataFetchingEnvironment.getLocalContext(); - Item item = dataFetchingEnvironment.getSource(); - return queryProfile(map.get(item.getSlug()).getProfileData().getUsername()); - } - - @DgsData(parentType = COMMENT.TYPE_NAME, field = COMMENT.Seller) - public Profile getCommentSeller(DataFetchingEnvironment dataFetchingEnvironment) { - Comment comment = dataFetchingEnvironment.getSource(); - Map 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(); - } -} diff --git a/.framework/java/backend/src/main/java/io/spring/graphql/RelationMutation.java b/.framework/java/backend/src/main/java/io/spring/graphql/RelationMutation.java deleted file mode 100644 index 317b4fcc..00000000 --- a/.framework/java/backend/src/main/java/io/spring/graphql/RelationMutation.java +++ /dev/null @@ -1,65 +0,0 @@ -package io.spring.graphql; - -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.DgsConstants.MUTATION; -import io.spring.graphql.exception.AuthenticationException; -import io.spring.graphql.types.Profile; -import io.spring.graphql.types.ProfilePayload; -import lombok.AllArgsConstructor; - -@DgsComponent -@AllArgsConstructor -public class RelationMutation { - - private UserRepository userRepository; - private 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(); - } -} diff --git a/.framework/java/backend/src/main/java/io/spring/graphql/SecurityUtil.java b/.framework/java/backend/src/main/java/io/spring/graphql/SecurityUtil.java deleted file mode 100644 index 24b723b2..00000000 --- a/.framework/java/backend/src/main/java/io/spring/graphql/SecurityUtil.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.spring.graphql; - -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 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); - } -} diff --git a/.framework/java/backend/src/main/java/io/spring/graphql/TagDatafetcher.java b/.framework/java/backend/src/main/java/io/spring/graphql/TagDatafetcher.java deleted file mode 100644 index 6b70bf5f..00000000 --- a/.framework/java/backend/src/main/java/io/spring/graphql/TagDatafetcher.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.spring.graphql; - -import com.netflix.graphql.dgs.DgsComponent; -import com.netflix.graphql.dgs.DgsData; -import io.spring.application.TagsQueryService; -import io.spring.graphql.DgsConstants.QUERY; -import java.util.List; -import lombok.AllArgsConstructor; - -@DgsComponent -@AllArgsConstructor -public class TagDatafetcher { - private TagsQueryService tagsQueryService; - - @DgsData(parentType = DgsConstants.QUERY_TYPE, field = QUERY.Tags) - public List getTags() { - return tagsQueryService.allTags(); - } -} diff --git a/.framework/java/backend/src/main/java/io/spring/graphql/UserMutation.java b/.framework/java/backend/src/main/java/io/spring/graphql/UserMutation.java deleted file mode 100644 index 581a5b7b..00000000 --- a/.framework/java/backend/src/main/java/io/spring/graphql/UserMutation.java +++ /dev/null @@ -1,93 +0,0 @@ -package io.spring.graphql; - -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.User; -import io.spring.core.user.UserRepository; -import io.spring.graphql.DgsConstants.MUTATION; -import io.spring.graphql.exception.GraphQLCustomizeExceptionHandler; -import io.spring.graphql.types.CreateUserInput; -import io.spring.graphql.types.UpdateUserInput; -import io.spring.graphql.types.UserPayload; -import io.spring.graphql.types.UserResult; -import java.util.Optional; -import javax.validation.ConstraintViolationException; -import lombok.AllArgsConstructor; -import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.crypto.password.PasswordEncoder; - -@DgsComponent -@AllArgsConstructor -public class UserMutation { - - private UserRepository userRepository; - private PasswordEncoder encryptService; - private UserService userService; - - @DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.CreateUser) - public DataFetcherResult createUser(@InputArgument("input") CreateUserInput input) { - RegisterParam registerParam = - new RegisterParam(input.getEmail(), input.getUsername(), input.getPassword()); - User user; - try { - user = userService.createUser(registerParam); - } catch (ConstraintViolationException cve) { - return DataFetcherResult.newResult() - .data(GraphQLCustomizeExceptionHandler.getErrorsAsData(cve)) - .build(); - } - - return DataFetcherResult.newResult() - .data(UserPayload.newBuilder().build()) - .localContext(user) - .build(); - } - - @DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.Login) - public DataFetcherResult login( - @InputArgument("password") String password, @InputArgument("email") String email) { - Optional optional = userRepository.findByEmail(email); - if (optional.isPresent() && encryptService.matches(password, optional.get().getPassword())) { - return DataFetcherResult.newResult() - .data(UserPayload.newBuilder().build()) - .localContext(optional.get()) - .build(); - } else { - throw new InvalidAuthenticationException(); - } - } - - @DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.UpdateUser) - public DataFetcherResult 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.newResult() - .data(UserPayload.newBuilder().build()) - .localContext(currentUser) - .build(); - } -} diff --git a/.framework/java/backend/src/main/java/io/spring/graphql/exception/AuthenticationException.java b/.framework/java/backend/src/main/java/io/spring/graphql/exception/AuthenticationException.java deleted file mode 100644 index 417029f7..00000000 --- a/.framework/java/backend/src/main/java/io/spring/graphql/exception/AuthenticationException.java +++ /dev/null @@ -1,3 +0,0 @@ -package io.spring.graphql.exception; - -public class AuthenticationException extends RuntimeException {} diff --git a/.framework/java/backend/src/main/java/io/spring/graphql/exception/GraphQLCustomizeExceptionHandler.java b/.framework/java/backend/src/main/java/io/spring/graphql/exception/GraphQLCustomizeExceptionHandler.java deleted file mode 100644 index bf4768b3..00000000 --- a/.framework/java/backend/src/main/java/io/spring/graphql/exception/GraphQLCustomizeExceptionHandler.java +++ /dev/null @@ -1,114 +0,0 @@ -package io.spring.graphql.exception; - -import com.netflix.graphql.dgs.exceptions.DefaultDataFetcherExceptionHandler; -import com.netflix.graphql.types.errors.ErrorType; -import com.netflix.graphql.types.errors.TypedGraphQLError; -import graphql.GraphQLError; -import graphql.execution.DataFetcherExceptionHandler; -import graphql.execution.DataFetcherExceptionHandlerParameters; -import graphql.execution.DataFetcherExceptionHandlerResult; -import io.spring.api.exception.FieldErrorResource; -import io.spring.api.exception.InvalidAuthenticationException; -import io.spring.graphql.types.Error; -import io.spring.graphql.types.ErrorItem; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import org.springframework.stereotype.Component; - -@Component -public class GraphQLCustomizeExceptionHandler implements DataFetcherExceptionHandler { - - private final DefaultDataFetcherExceptionHandler defaultHandler = - new DefaultDataFetcherExceptionHandler(); - - @Override - public DataFetcherExceptionHandlerResult onException( - DataFetcherExceptionHandlerParameters handlerParameters) { - if (handlerParameters.getException() instanceof InvalidAuthenticationException) { - GraphQLError graphqlError = - TypedGraphQLError.newBuilder() - .errorType(ErrorType.UNAUTHENTICATED) - .message(handlerParameters.getException().getMessage()) - .path(handlerParameters.getPath()) - .build(); - return DataFetcherExceptionHandlerResult.newResult().error(graphqlError).build(); - } else if (handlerParameters.getException() instanceof ConstraintViolationException) { - List errors = new ArrayList<>(); - for (ConstraintViolation violation : - ((ConstraintViolationException) handlerParameters.getException()) - .getConstraintViolations()) { - FieldErrorResource fieldErrorResource = - new FieldErrorResource( - violation.getRootBeanClass().getName(), - getParam(violation.getPropertyPath().toString()), - violation - .getConstraintDescriptor() - .getAnnotation() - .annotationType() - .getSimpleName(), - violation.getMessage()); - errors.add(fieldErrorResource); - } - GraphQLError graphqlError = - TypedGraphQLError.newBadRequestBuilder() - .message(handlerParameters.getException().getMessage()) - .path(handlerParameters.getPath()) - .extensions(errorsToMap(errors)) - .build(); - return DataFetcherExceptionHandlerResult.newResult().error(graphqlError).build(); - } else { - return defaultHandler.onException(handlerParameters); - } - } - - public static Error getErrorsAsData(ConstraintViolationException cve) { - List errors = new ArrayList<>(); - for (ConstraintViolation violation : cve.getConstraintViolations()) { - FieldErrorResource fieldErrorResource = - new FieldErrorResource( - violation.getRootBeanClass().getName(), - getParam(violation.getPropertyPath().toString()), - violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName(), - violation.getMessage()); - errors.add(fieldErrorResource); - } - Map> errorMap = new HashMap<>(); - for (FieldErrorResource fieldErrorResource : errors) { - if (!errorMap.containsKey(fieldErrorResource.getField())) { - errorMap.put(fieldErrorResource.getField(), new ArrayList<>()); - } - errorMap.get(fieldErrorResource.getField()).add(fieldErrorResource.getMessage()); - } - List errorItems = - errorMap.entrySet().stream() - .map(kv -> ErrorItem.newBuilder().key(kv.getKey()).value(kv.getValue()).build()) - .collect(Collectors.toList()); - return Error.newBuilder().message("BAD_REQUEST").errors(errorItems).build(); - } - - private static String getParam(String s) { - String[] splits = s.split("\\."); - if (splits.length == 1) { - return s; - } else { - return String.join(".", Arrays.copyOfRange(splits, 2, splits.length)); - } - } - - private static Map errorsToMap(List errors) { - Map json = new HashMap<>(); - for (FieldErrorResource fieldErrorResource : errors) { - if (!json.containsKey(fieldErrorResource.getField())) { - json.put(fieldErrorResource.getField(), new ArrayList<>()); - } - ((List) json.get(fieldErrorResource.getField())).add(fieldErrorResource.getMessage()); - } - return json; - } -} diff --git a/.framework/java/backend/src/main/resources/schema/schema.graphqls b/.framework/java/backend/src/main/resources/schema/schema.graphqls deleted file mode 100644 index 7a73b4d8..00000000 --- a/.framework/java/backend/src/main/resources/schema/schema.graphqls +++ /dev/null @@ -1,177 +0,0 @@ -# Build the schema. -type Query { - item(slug: String!): Item - items( - first: Int, - after: String, - last: Int, - before: String, - soldBy: String - favoritedBy: String - withTag: String - ): ItemsConnection - me: User - feed(first: Int, after: String, last: Int, before: String): ItemsConnection - profile(username: String!): ProfilePayload - tags: [String] -} - -union UserResult = UserPayload | Error - -type Mutation { - ### User & Profile - createUser(input: CreateUserInput): UserResult - login(password: String!, email: String!): UserPayload - updateUser(changes: UpdateUserInput!): UserPayload - followUser(username: String!): ProfilePayload - unfollowUser(username: String!): ProfilePayload - - ### Item - createItem(input: CreateItemInput!): ItemPayload - updateItem(slug: String!, changes: UpdateItemInput!): ItemPayload - favoriteItem(slug: String!): ItemPayload - unfavoriteItem(slug: String!): ItemPayload - deleteItem(slug: String!): DeletionStatus - - ### Comment - addComment(slug: String!, body: String!): CommentPayload - deleteComment(slug: String!, id: ID!): DeletionStatus -} - -schema { - query: Query - mutation: Mutation -} - -### Items -type Item { - seller: Profile! - comments(first: Int, after: String, last: Int, before: String): CommentsConnection - createdAt: String! - description: String! - favorited: Boolean! - favoritesCount: Int! - image: String! - slug: String! - tagList: [String], - title: String! - updatedAt: String! -} - -type ItemEdge { - cursor: String! - node: Item -} - -type ItemsConnection { - edges: [ItemEdge] - pageInfo: PageInfo! -} - -### Comments -type Comment { - id: ID! - seller: Profile! - item: Item! - 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 - items(first: Int, after: String, last: Int, before: String): ItemsConnection - favorites(first: Int, after: String, last: Int, before: String): ItemsConnection - feed(first: Int, after: String, last: Int, before: String): ItemsConnection -} - -### User -type User { - email: String! - profile: Profile! - token: String! - username: String! -} - -### Error -type Error { - message: String - errors: [ErrorItem!] -} - -type ErrorItem { - key: String! - value: [String!]! -} - -## Mutations - -# Input types. -input UpdateItemInput { - description: String - image: String - title: String -} - -input CreateItemInput { - description: String! - image: String! - tagList: [String] - title: String! -} - -type ItemPayload { - item: Item -} - -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 -} -