Skip to content
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

Merged
merged 6 commits into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,20 @@
<testcontainers.version>1.18.0</testcontainers.version>
<elasticsearch.version>5.1.0</elasticsearch.version>
<prometheus.version>1.11.0</prometheus.version>
<graphql.validation.version>20.0</graphql.validation.version>
</properties>

<dependencies>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-extended-validation</artifactId>
<version>${graphql.validation.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
Expand Down
35 changes: 35 additions & 0 deletions src/main/java/com/solvd/micro9/users/web/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.solvd.micro9.users.web.controller.graphql.LocalDateTimeToStringCoercing;
import graphql.schema.GraphQLScalarType;
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;
Expand Down Expand Up @@ -43,4 +49,33 @@ public WebClient.Builder webClient() {
return WebClient.builder();
}

@Bean
public GraphQLScalarType localDateTimeScalar(
final LocalDateTimeToStringCoercing coercing
) {
return GraphQLScalarType.newScalar()
.name("LocalDateTime")
.description("Java 8 LocalDateTime as scalar")
.coercing(coercing)
.build();
}

@Bean
public ValidationSchemaWiring schemaWiring() {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Owner Author

Choose a reason for hiding this comment

The 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?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eyakauleva for code reuse?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@h1alexbel but I call this method only once

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eyakauleva I know, but what about future usages?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@h1alexbel according to YAGNI principle features should only be added when required

Copy link
Collaborator

@h1alexbel h1alexbel Jun 1, 2023

Choose a reason for hiding this comment

The 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

Copy link
Owner Author

Choose a reason for hiding this comment

The 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(
final GraphQLScalarType scalarType,
final ValidationSchemaWiring schemaWiring
) {
return wiringBuilder -> wiringBuilder
.scalar(scalarType)
.directiveWiring(schemaWiring);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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 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;

@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,
@Argument final String commandBy
) {
CreateUserCommand command = new CreateUserCommand(user, commandBy);
return commandHandler.apply(command);
}

@MutationMapping("deleteUser")
public Mono<EsUser> delete(
@Argument final String id,
@Argument final String commandBy
) {
DeleteUserCommand command = new DeleteUserCommand(id, commandBy);
return commandHandler.apply(command);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.solvd.micro9.users.web.dto.TicketDto;
import com.solvd.micro9.users.web.dto.UserDto;
import com.solvd.micro9.users.web.dto.criteria.UserCriteriaDto;
import com.solvd.micro9.users.web.mapper.EsMapper;
import com.solvd.micro9.users.web.mapper.UserCriteriaMapper;
import com.solvd.micro9.users.web.mapper.UserMapper;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
Expand All @@ -35,20 +36,22 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
public class RestUserController {

@Value("${ticket-service}")
private String ticketService;
private final EsUserCommandHandler commandHandler;
private final UserQueryHandler queryHandler;
private final UserMapper userMapper;
private final EsMapper esMapper;
private final WebClient.Builder webClientBuilder;
private final UserCriteriaMapper criteriaMapper;
private static final String USER_SERVICE = "user-service";
Expand Down Expand Up @@ -117,16 +120,24 @@ public Mono<EsDto> createTicket(
}

@PostMapping
public Mono<EsUser> create(@RequestBody @Validated final UserDto userDto) {
public Mono<EsDto> create(
@RequestHeader("command_by") final String commandBy,
@RequestBody @Validated final UserDto userDto
) {
User user = userMapper.dtoToDomain(userDto);
CreateUserCommand command = new CreateUserCommand(user, "Liza123");
return commandHandler.apply(command);
CreateUserCommand command = new CreateUserCommand(user, commandBy);
Mono<EsUser> esUserMono = commandHandler.apply(command);
return esMapper.domainToDto(esUserMono);
}

@DeleteMapping(value = "/{id}")
public Mono<EsUser> delete(@PathVariable("id") final String id) {
DeleteUserCommand command = new DeleteUserCommand(id, "Liza123");
return commandHandler.apply(command);
public Mono<EsDto> delete(
@RequestHeader("command_by") final String commandBy,
@PathVariable("id") final String id
) {
DeleteUserCommand command = new DeleteUserCommand(id, commandBy);
Mono<EsUser> esUserMono = commandHandler.apply(command);
return esMapper.domainToDto(esUserMono);
}

@SneakyThrows
Expand Down
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 GraphqlExceptionResolver extends DataFetcherExceptionResolverAdapter {

@Override
protected GraphQLError resolveToSingleError(
final Throwable ex, final DataFetchingEnvironment env
) {
return GraphqlErrorBuilder.newError()
.errorType(ErrorType.BAD_REQUEST)
.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,58 @@
package com.solvd.micro9.users.web.controller.graphql;

import graphql.language.StringValue;
import graphql.schema.Coercing;
import graphql.schema.CoercingParseLiteralException;
import graphql.schema.CoercingParseValueException;
import graphql.schema.CoercingSerializeException;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

@Component
public class LocalDateTimeToStringCoercing implements 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) {
return this.parse(input);
}

@Override
public LocalDateTime parseLiteral(final Object input) {
return this.parse(input);
}

private LocalDateTime parse(final Object input) {
try {
if (input instanceof StringValue) {
return LocalDateTime.parse(((StringValue) input).getValue());
} else if (input instanceof String) {
return LocalDateTime.parse((String) input);
} else {
throw new CoercingParseLiteralException(
"Expected String or StringValue"
);
}
} catch (DateTimeParseException e) {
throw new CoercingParseValueException(
String.format("Not a valid date: '%s'.", input), e
);
}
}

}
17 changes: 17 additions & 0 deletions src/main/java/com/solvd/micro9/users/web/mapper/EsMapper.java
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);
}

}
3 changes: 3 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ logging:
name: logs/application-info.log

spring:
graphql:
graphiql:
enabled: true
config:
import: optional:file:.env[.properties]
jackson:
Expand Down
83 changes: 83 additions & 0 deletions src/main/resources/graphql/schema.graphqls
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!, commandBy: String!): Es
deleteUser(id: ID!, commandBy: String!): 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
}