-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
graphql #50
graphql #50
Changes from 5 commits
c33783e
adfd385
a555821
84f9f7e
4ebd670
679c337
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,16 +2,27 @@ | |
|
||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; | ||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; | ||
import graphql.language.StringValue; | ||
import graphql.schema.Coercing; | ||
import graphql.schema.CoercingParseValueException; | ||
import graphql.schema.CoercingSerializeException; | ||
import graphql.schema.GraphQLScalarType; | ||
import graphql.schema.CoercingParseLiteralException; | ||
import graphql.validation.rules.OnValidationErrorStrategy; | ||
import graphql.validation.rules.ValidationRules; | ||
import graphql.validation.schemawiring.ValidationSchemaWiring; | ||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.data.web.ReactivePageableHandlerMethodArgumentResolver; | ||
import org.springframework.graphql.execution.RuntimeWiringConfigurer; | ||
import org.springframework.web.reactive.config.WebFluxConfigurer; | ||
import org.springframework.web.reactive.function.client.WebClient; | ||
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; | ||
|
||
import java.time.LocalDateTime; | ||
import java.time.format.DateTimeFormatter; | ||
import java.time.format.DateTimeParseException; | ||
|
||
@Configuration | ||
public class WebConfig implements WebFluxConfigurer { | ||
|
@@ -43,4 +54,69 @@ public WebClient.Builder webClient() { | |
return WebClient.builder(); | ||
} | ||
|
||
public GraphQLScalarType localDateTimeScalar() { | ||
return GraphQLScalarType.newScalar() | ||
.name("LocalDateTime") | ||
.description("Java 8 LocalDateTime as scalar") | ||
.coercing(new Coercing<LocalDateTime, String>() { | ||
@Override | ||
public String serialize(final Object input) { | ||
if (input instanceof LocalDateTime) { | ||
DateTimeFormatter formatter = | ||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); | ||
return ((LocalDateTime) input).format(formatter); | ||
} else { | ||
throw new CoercingSerializeException( | ||
"Expected a LocalDateTime object" | ||
); | ||
} | ||
} | ||
|
||
@Override | ||
public LocalDateTime parseValue(final Object input) { | ||
try { | ||
if (input instanceof String) { | ||
return LocalDateTime.parse((String) input); | ||
} else { | ||
throw new CoercingParseValueException("Expected a String"); | ||
} | ||
} catch (DateTimeParseException e) { | ||
throw new CoercingParseValueException( | ||
String.format("Not a valid date: '%s'.", input), e | ||
); | ||
} | ||
} | ||
|
||
@Override | ||
public LocalDateTime parseLiteral(final Object input) { | ||
if (input instanceof StringValue) { | ||
try { | ||
return LocalDateTime.parse(((StringValue) input) | ||
.getValue()); | ||
} catch (DateTimeParseException e) { | ||
throw new CoercingParseLiteralException(e); | ||
} | ||
} else { | ||
throw new CoercingParseLiteralException( | ||
"Expected a StringValue" | ||
); | ||
} | ||
} | ||
}).build(); | ||
} | ||
|
||
public ValidationSchemaWiring schemaWiring() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @eyakauleva as far I understand this ValidationSchemaWiring also should be represented as Spring Bean There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @h1alexbel I call this method in another method, then why it should be a bean? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @eyakauleva for code reuse? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @h1alexbel but I call this method only once There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @eyakauleva I know, but what about future usages? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @h1alexbel according to YAGNI principle features should only be added when required There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @eyakauleva YAGNI is about features or FR, not about maintainability, which is NFR try to design all your classes, methods, functions with maintainability in mind There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done 679c337 |
||
ValidationRules validationRules = ValidationRules.newValidationRules() | ||
.onValidationErrorStrategy(OnValidationErrorStrategy.RETURN_NULL) | ||
.build(); | ||
return new ValidationSchemaWiring(validationRules); | ||
} | ||
|
||
@Bean | ||
public RuntimeWiringConfigurer runtimeWiringConfigurer() { | ||
return wiringBuilder -> wiringBuilder | ||
.scalar(localDateTimeScalar()) | ||
.directiveWiring(schemaWiring()); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package com.solvd.micro9.users.web.controller; | ||
|
||
import com.solvd.micro9.users.domain.aggregate.User; | ||
import com.solvd.micro9.users.domain.command.CreateUserCommand; | ||
import com.solvd.micro9.users.domain.command.DeleteUserCommand; | ||
import com.solvd.micro9.users.domain.criteria.UserCriteria; | ||
import com.solvd.micro9.users.domain.es.EsUser; | ||
import com.solvd.micro9.users.domain.query.EsUserQuery; | ||
import com.solvd.micro9.users.service.EsUserCommandHandler; | ||
import com.solvd.micro9.users.service.UserQueryHandler; | ||
import com.solvd.micro9.users.web.mapper.UserCriteriaMapper; | ||
import com.solvd.micro9.users.web.mapper.UserMapper; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.data.domain.PageRequest; | ||
import org.springframework.data.domain.Pageable; | ||
import org.springframework.graphql.data.method.annotation.Argument; | ||
import org.springframework.graphql.data.method.annotation.MutationMapping; | ||
import org.springframework.graphql.data.method.annotation.QueryMapping; | ||
import org.springframework.stereotype.Controller; | ||
import reactor.core.publisher.Flux; | ||
import reactor.core.publisher.Mono; | ||
|
||
@Controller | ||
@RequiredArgsConstructor | ||
public class GraphqlUserController { | ||
|
||
private final EsUserCommandHandler commandHandler; | ||
private final UserQueryHandler queryHandler; | ||
private final UserMapper userMapper; | ||
private final UserCriteriaMapper criteriaMapper; | ||
|
||
@QueryMapping("getAllUsers") | ||
public Flux<User> getAll() { | ||
return queryHandler.getAll(); | ||
} | ||
|
||
@QueryMapping("findByCriteria") | ||
public Flux<User> findByCriteria(@Argument final UserCriteria criteria, | ||
@Argument final int size, | ||
@Argument final int page) { | ||
Pageable pageable = PageRequest.of(page, size); | ||
return queryHandler.findByCriteria(criteria, pageable); | ||
} | ||
|
||
@QueryMapping("findUserById") | ||
public Mono<User> findByUserId(@Argument final String userId) { | ||
EsUserQuery query = new EsUserQuery(userId); | ||
return queryHandler.findById(query); | ||
} | ||
|
||
@MutationMapping("createUser") | ||
public Mono<EsUser> create(@Argument final User user) { | ||
CreateUserCommand command = new CreateUserCommand(user, "Liza123"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @eyakauleva what is the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed 679c337 |
||
return commandHandler.apply(command); | ||
} | ||
|
||
@MutationMapping("deleteUser") | ||
public Mono<EsUser> delete(@Argument final String id) { | ||
DeleteUserCommand command = new DeleteUserCommand(id, "Liza123"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @eyakauleva the same goes here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed 679c337 |
||
return commandHandler.apply(command); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package com.solvd.micro9.users.web.controller.exception; | ||
|
||
import graphql.GraphQLError; | ||
import graphql.GraphqlErrorBuilder; | ||
import graphql.schema.DataFetchingEnvironment; | ||
import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter; | ||
import org.springframework.graphql.execution.ErrorType; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class ExceptionResolver extends DataFetcherExceptionResolverAdapter { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @eyakauleva nice solution since GraphQL always throws There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @h1alexbel GraphQL still returns 200 HTTP status code. This resolver sets status code only as a field to a response body and also sets custom error message there. |
||
|
||
@Override | ||
protected GraphQLError resolveToSingleError( | ||
final Throwable ex, final DataFetchingEnvironment env | ||
) { | ||
return GraphqlErrorBuilder.newError() | ||
.errorType(ErrorType.NOT_FOUND) | ||
.message(ex.getMessage()) | ||
.path(env.getExecutionStepInfo().getPath()) | ||
.location(env.getField().getSourceLocation()) | ||
.build(); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package com.solvd.micro9.users.web.mapper; | ||
|
||
import com.solvd.micro9.users.domain.es.EsUser; | ||
import com.solvd.micro9.users.web.dto.EsDto; | ||
import org.mapstruct.Mapper; | ||
import reactor.core.publisher.Mono; | ||
|
||
@Mapper(componentModel = "spring") | ||
public interface EsMapper { | ||
|
||
EsDto domainToDto(EsUser esUser); | ||
|
||
default Mono<EsDto> domainToDto(Mono<EsUser> esUserMono) { | ||
return esUserMono.map(this::domainToDto); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
scalar LocalDateTime | ||
|
||
directive @NotBlank(message : String = "graphql.validation.NotBlank.message") | ||
on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ||
directive @Min(value : Int = 0, message : String = "graphql.validation.Min.message") | ||
on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ||
|
||
type Query { | ||
getAllUsers: [User] | ||
findUserById(userId: ID!): User | ||
findByCriteria(criteria: CriteriaInput!, size: Int! @Min(value: 1), page: Int! @Min): [User] | ||
} | ||
|
||
type Mutation { | ||
createUser(user: UserInput!): Es | ||
deleteUser(id: ID!): Es | ||
} | ||
|
||
type User { | ||
id: ID | ||
firstName: String! | ||
lastName: String! | ||
email: String! | ||
phone: String! | ||
age: Int | ||
gender: Gender | ||
height: Float | ||
weight: Float | ||
eyesColor: EyesColor | ||
startStudyYear: Int | ||
endStudyYear: Int | ||
} | ||
|
||
enum Gender { | ||
MALE | ||
FEMALE | ||
UNSET | ||
} | ||
|
||
enum EyesColor { | ||
BLUE | ||
GREEN | ||
BROWN | ||
GREY | ||
UNSET | ||
} | ||
|
||
input UserInput { | ||
firstName: String @NotBlank | ||
lastName: String @NotBlank | ||
email: String @NotBlank | ||
phone: String @NotBlank | ||
age: Int | ||
gender: Gender | ||
height: Float | ||
weight: Float | ||
eyesColor: EyesColor | ||
startStudyYear: Int | ||
endStudyYear: Int | ||
} | ||
|
||
input CriteriaInput { | ||
name: String | ||
phone: String | ||
age: Int | ||
heightFrom: Float | ||
heightTo: Float | ||
weightFrom: Float | ||
weightTo: Float | ||
genders: [Gender] | ||
eyesColors: [EyesColor] | ||
studyYear: Int | ||
} | ||
|
||
type Es { | ||
id: ID | ||
type: String | ||
time: LocalDateTime | ||
createdBy: String | ||
entityId: String | ||
payload: String | ||
status: String | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@eyakauleva try to make more elegant solution here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done 679c337