├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── build.gradle ├── docs └── images │ ├── graphiql.gif │ ├── h2-console.gif │ └── voyager.gif ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── settings.gradle ├── sonar-project.properties └── src ├── main ├── kotlin │ └── com │ │ └── andrejusti │ │ └── example │ │ └── kotlin │ │ └── springboot │ │ └── graphql │ │ ├── Application.kt │ │ ├── domain │ │ ├── controller │ │ │ └── graphql │ │ │ │ ├── config │ │ │ │ └── CustomGraphQlErrorHandler.kt │ │ │ │ └── resolver │ │ │ │ ├── CategoryGraphQlResolver.kt │ │ │ │ └── ProductGraphQlResolver.kt │ │ ├── entity │ │ │ ├── Category.kt │ │ │ └── Product.kt │ │ ├── repository │ │ │ └── api │ │ │ │ ├── CategoryRepository.kt │ │ │ │ └── ProductRepository.kt │ │ └── service │ │ │ ├── CategoryService.kt │ │ │ └── ProductService.kt │ │ └── infrastructure │ │ ├── dto │ │ ├── ItemValidationError.kt │ │ └── Pagination.kt │ │ ├── exception │ │ └── ValidationException.kt │ │ ├── package-info.java │ │ └── service │ │ ├── PaginationService.kt │ │ └── ValidateService.kt └── resources │ ├── application.yml │ ├── banner.txt │ ├── db │ └── changelog │ │ └── db.changelog-master.yaml │ └── graphql │ ├── inputs.graphqls │ ├── mutations.graphqls │ ├── querys.graphqls │ └── types.graphqls └── test └── kotlin └── com └── andrejusti └── example └── kotlin └── springboot └── graphql └── domain ├── AbstractIT.kt ├── it ├── CategoryIT.kt └── ProductIT.kt └── sdk ├── AbstractSdk.kt ├── CategorySdk.kt └── ProductSdk.kt /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /bin/ 3 | /vscode/ 4 | /.vscode/ 5 | .vscode/settings.json 6 | /build/ 7 | /target/ 8 | /settins/ 9 | !gradle/wrapper/gradle-wrapper.jar 10 | .apt_generated 11 | .classpath 12 | .factorypath 13 | .project 14 | .settings 15 | .springBeans 16 | .sts4-cache 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | /out/ 22 | /nbproject/private/ 23 | /build/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | 3 | env: 4 | - DOCKER_IMAGE_APP="justiandre/example-kotlin-springboot-graphql:$TRAVIS_BRANCH" 5 | 6 | sudo: required 7 | 8 | language: kotlin 9 | 10 | jdk: 11 | - oraclejdk8 12 | 13 | cache: 14 | directories: 15 | - '$HOME/.sonar/cache' 16 | 17 | services: 18 | - docker 19 | 20 | addons: 21 | sonarcloud: 22 | organization: "justiandre-github" 23 | 24 | script: 25 | - ./gradlew clean build 26 | - docker build -t $DOCKER_IMAGE_APP . 27 | - docker login -u "$DOCKER_AUTH_USER" -p "$DOCKER_AUTH_PASS" 28 | - docker push $DOCKER_IMAGE_APP 29 | - sonar-scanner 30 | 31 | after_success: 32 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM andreptb/oracle-java:8-alpine 2 | 3 | WORKDIR /data/ 4 | 5 | ADD build/libs/*.jar app.jar 6 | 7 | ARG PROFILES 8 | ARG PORT 9 | ARG SPRING_PROFILES_ACTIVE 10 | ARG JAVA_OPTS 11 | ARG SERVER_PORT 12 | ARG PATH_JAR 13 | 14 | ENV SPRING_PROFILES_ACTIVE ${SPRING_PROFILES_ACTIVE:-${PROFILES:-default}} 15 | ENV JAVA_OPTS ${JAVA_OPTS:-'-Xmx2g'} 16 | ENV SERVER_PORT ${SERVER_PORT:-${PORT:-8080}} 17 | 18 | EXPOSE ${SERVER_PORT} 19 | 20 | CMD java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar app.jar -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 André Justi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # example-kotlin-springboot-graphql 2 | 3 | [![Build Status](https://travis-ci.org/justiandre/example-kotlin-springboot-graphql.svg?branch=master)](https://travis-ci.org/justiandre/example-kotlin-springboot-graphql) [![Docker Automated build](https://img.shields.io/docker/automated/jrottenberg/ffmpeg.svg)](https://hub.docker.com/r/justiandre/example-kotlin-springboot-graphql/tags/) [![codecov](https://codecov.io/gh/justiandre/example-kotlin-springboot-graphql/branch/master/graph/badge.svg)](https://codecov.io/gh/justiandre/example-kotlin-springboot-graphql) [![codebeat badge](https://codebeat.co/badges/f51bed18-2d41-4336-b7af-bc1722d412a1)](https://codebeat.co/projects/github-com-justiandre-example-kotlin-springboot-graphql-master) [![reliability_rating](https://sonarcloud.io/api/project_badges/measure?project=justiandre_example-kotlin-springboot-graphql&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=justiandre_example-kotlin-springboot-graphql) [![bugs](https://sonarcloud.io/api/project_badges/measure?project=justiandre_example-kotlin-springboot-graphql&metric=bugs)](https://sonarcloud.io/project/issues?id=justiandre_example-kotlin-springboot-graphql&resolved=false&types=BUG) [![duplicated_lines_density](https://sonarcloud.io/api/project_badges/measure?project=justiandre_example-kotlin-springboot-graphql&metric=duplicated_lines_density)](https://sonarcloud.io/component_measures?id=justiandre_example-kotlin-springboot-graphql&metric=duplicated_lines_density) [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin) 4 | 5 | 6 | Projeto de exemplo com API GraphQL com dois crud’s simples, utilizando Kotlin, Spring Boot, Gradle etc. 7 | 8 | ## Tecnologias utilizadas 9 | - [Kotlin](https://kotlinlang.org) 10 | - [Spring Boot](https://projects.spring.io/spring-boot/) 11 | - [Gradle](https://gradle.org) 12 | - [Docker](https://www.docker.com) 13 | - [GraphQL](https://graphql.org) 14 | - [GraphQL Voyager](https://github.com/APIs-guru/graphql-voyager) 15 | - [GraphQL GraphiQL](https://github.com/graphql/graphiql) 16 | - [Spring Data](http://projects.spring.io/spring-data/) 17 | - [Liquibase](https://www.liquibase.org) 18 | - [H2 Database](http://www.h2database.com/html/main.html) 19 | - [Jacoco](https://www.eclemma.org/jacoco/) 20 | 21 | 22 | ## Utilização e Build 23 | 24 | Após a inicialização da aplicação, independente da forma, a mesma pode ser acessada ou ter a sua documentação acessada pelo link `http://localhost:PORTA_INICIADA/`. 25 | 26 | **OBS:** Ao iniciar a aplicação, também é iniciado um banco de dados, H2 embutido, então não é necessário nenhuma dependência externa para a mesma ser iniciada e acessada. 27 | 28 | ### Links úteis 29 | 30 | Após o sistema ser iniciado podem ser acessadas as seguintes URLs. 31 | 32 | #### [GraphQL](https://graphql.org) - [/graphql](http://localhost:8080/graphql) 33 | 34 | URL para acesso a todos os recursos GraphQL da aplicação. 35 | 36 | **URL:** `http://localhost:8080/graphql` ou `http://localhost:PORTA_INICIADA/graphql`. 37 | 38 | #### [GraphQL/GraphIQL](https://github.com/graphql-java/graphql-spring-boot) - [/graphiql](http://localhost:8080/graphiql) 39 | 40 | URL para acesso a uma IDE iterativa para operações GraphQL. 41 | 42 | **URL:** `http://localhost:8080/graphiql` ou `http://localhost:PORTA_INICIADA/graphiql`. 43 | 44 | ![GraphIQL](docs/images/graphiql.gif) 45 | 46 | #### [GraphIQL/Voyager](https://github.com/APIs-guru/graphql-voyager) - [/voyager](http://localhost:8080/voyager) 47 | 48 | URL para acesso a um gráfico iterativa para operações GraphQL. 49 | 50 | **URL:** `http://localhost:8080/voyager` ou `http://localhost:PORTA_INICIADA/voyager`. 51 | 52 | ![Voyager](docs/images/voyager.gif) 53 | 54 | #### [H2 Database](http://www.h2database.com/html/main.html) - [/h2-console](http://localhost:8080/h2-console) 55 | 56 | URL para acesso a base de dados. 57 | 58 | **URL:** `http://localhost:8080/h2-console` ou `http://localhost:PORTA_INICIADA/h2-console`. 59 | 60 | **Dados de acesso** 61 | 62 | * Setting Name: `Generic H2 (Embedded)` 63 | * Driver Class: `org.h2.Driver` 64 | * JDBC URL: `jdbc:h2:mem:testdb` 65 | * User Name: `sa` 66 | * Password: `` (vazio) 67 | 68 | ![H2 Database](docs/images/h2-console.gif) 69 | 70 | ### Utilização em ambiente local 71 | 72 | Localmente o projeto pode ser configurado nas ide’s [IntelliJ](https://www.jetbrains.com/idea/) e [Eclipse](https://www.eclipse.org/ide/), ambas possuem suporte via plugin para Kotlin, Spring Boot e Gradle. 73 | 74 | ### Build usando artefatos locais (GIT) 75 | 76 | **OBS:** Os arquivos e ferramentas de build, estão contidas dentro do projeto, então para execução do build, só é necessário ter o [Java](https://www.oracle.com/br/java/index.html) ou o Docker instalado. 77 | 78 | ### Build local com Gradle 79 | 80 | ```shell 81 | # Execução do build 82 | ./gradlew build 83 | # Execução da aplicação 84 | java -jar build/libs/example-kotlin-springboot-graphql-0.0.1-SNAPSHOT 85 | ``` 86 | 87 | **OBS:** Com esses comandos a aplicação será iniciada, na porta 8080, mas isso pode ser alterado, informando o parâmetro: `-Dserver.port=$PORTA_DESEJADA`. 88 | 89 | ### Build local com Docker 90 | 91 | ```shell 92 | # Execução do build 93 | docker build -t app:latest . 94 | # Execução da aplicação 95 | docker run -d -p 8080:8080 app:latest 96 | ``` 97 | 98 | **OBS:** Com esses comandos a aplicação será iniciada, na porta 8080, mas isso pode ser alterado, informando o parâmetro docker: `-p $PORTA_DESEJADA:8080`. 99 | 100 | ### Utilização com Docker (imagem remota do DockerHub) 101 | 102 | ```shell 103 | # Execução da aplicação 104 | docker run -d -p 8080:8080 justiandre/example-kotlin-springboot-graphql:master 105 | ``` 106 | 107 | **OBS:** Com esse comando a aplicação será iniciada, na porta 8080, mas isso pode ser alterado, informando o parâmetro docker: `-p $PORTA_DESEJADA:8080****************`. 108 | 109 | ## Arquivo e artefatos do projeto 110 | 111 | | Arquivo ou diretório | Descrição | 112 | | ------------ | ------------ | 113 | | .travis.yml | Arquivo com as configurações de build contínuo, no caso desse projeto possui as configurações de build, testes e push da imagem Docker para o DockerHub | 114 | | Dockerfile | Arquivo com as configurações para criação da imagem Docker | 115 | | gradle/*, settings.gradle, gradlew.sh, gradlew.bat | Arquivos e diretórios de instalação do Gradle, com eles no projeto, não é necessário instalar o Gradle separadamente | 116 | | src/main/kotlin | Diretório com os fonts Kotlin da aplicação | 117 | | src/main/kotlin/tech/justi/example/kotlin/springboot/graphql/Application.kt | Classe responsável pela inicialização da aplicação | 118 | | src/main/kotlin/tech/justi/example/kotlin/springboot/graphql/domain/controller/graphql/config | Pacote com as classes com as configurações referentes ao graphql como tratamento de exceptions | 119 | | src/main/kotlin/tech/justi/example/kotlin/springboot/graphql/domain/controller/graphql/resolver | Pacote com as classes com os resolvers (endpoints) do graphql, tanto as de querys quanto as de mutação | 120 | | src/main/kotlin/tech/justi/example/kotlin/springboot/graphql/domain/entity | Pacote com as entidades de mapeamento JPA | 121 | | src/main/kotlin/tech/justi/example/kotlin/springboot/graphql/domain/repository | Pacote com as classes de acesso aos dados externos a aplicação | 122 | | src/main/kotlin/tech/justi/example/kotlin/springboot/graphql/domain/service | Pacote com as classes de regra de negocio do sistema | 123 | | src/main/kotlin/tech/justi/example/kotlin/springboot/graphql/infrastructure | O intuito desse pacote é manter as classes responsáveis pela infraestrutura da aplicação ou classes utilitárias, o fato é que esse pacote não deveria existir, o tal código visto aqui, deveria estar em componentes isolados fora da aplicação, podendo ser reaproveitado também em outras aplicações e fazendo um isolamento do codigo de negócio do codigo de infraestrutura ou utilitários. | 124 | | src/main/resources | Diretório com as configurações do sistema | 125 | | src/main/resources/application.yml | Arquivo de configuração do Spring Boot | 126 | | src/main/resources/banner.txt | Banner customizado do Spring Boot | 127 | | src/main/resources/db/changelog | Diretório com os arquivos de versionamento do banco de dados | 128 | | src/main/resources/graphql | Diretório com as configurações e mapeamento graphql | 129 | | src/test | Diretório com os fontes e configurações de teste | 130 | 131 | ## Log ao iniciar a aplicação. 132 | 133 | ```shell 134 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 135 | + __ __ + 136 | + /\ \ __ /\ \__ __ + 137 | + __ ___ \_\ \ _ __ __ /\_\ __ __ ___\ \ ,_\/\_\ ___ ___ ___ ___ + 138 | + /'__`\ /' _ `\ /'_` \/\`'__\/'__`\\/\ \/\ \/\ \ /',__\ \ \/\/\ \ /'___\ / __`\ /' __` __`\ + 139 | + /\ \L\.\_/\ \/\ \/\ \L\ \ \ \//\ __/ \ \ \ \ \_\ \/\__, `\ \ \_\ \ \ __/\ \__//\ \L\ \/\ \/\ \/\ \ + 140 | + \ \__/.\_\ \_\ \_\ \___,_\ \_\\ \____\_\ \ \ \____/\/\____/\ \__\\ \_\/\_\ \____\ \____/\ \_\ \_\ \_\ + 141 | + \/__/\/_/\/_/\/_/\/__,_ /\/_/ \/____/\ \_\ \/___/ \/___/ \/__/ \/_/\/_/\/____/\/___/ \/_/\/_/\/_/ + 142 | + \ \____/ + 143 | + \/___/ + 144 | + + 145 | + App de exemplo: Kotlin + Spring Boot + GraphQL + 146 | + https://github.com/justiandre/example-kotlin-springboot-graphql + 147 | + Por: André Justi - http://andrejusti.com + 148 | + + 149 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 150 | 151 | 152 | $datetime INFO --- [ main] t.j.e.k.s.graphql.ApplicationKt : Starting ApplicationKt on 108c4239b213 with PID 7 (/data/app.jar started by root in /data) 153 | $datetime INFO --- [ main] t.j.e.k.s.graphql.ApplicationKt : The following profiles are active: default 154 | $datetime INFO --- [ main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@32eebfca: startup date [Sun Sep 16 23:44:27 GMT 2018]; root of context hierarchy 155 | $datetime INFO --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$ace23b85] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 156 | $datetime INFO --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 157 | $datetime INFO --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 158 | $datetime INFO --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.32 159 | $datetime INFO --- [ost-startStop-1] o.a.catalina.core.AprLifecycleListener : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib] 160 | $datetime INFO --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 161 | $datetime INFO --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 2386 ms 162 | $datetime INFO --- [ost-startStop-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 163 | $datetime INFO --- [ost-startStop-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 164 | $datetime INFO --- [ost-startStop-1] liquibase : Successfully acquired change log lock 165 | $datetime INFO --- [ost-startStop-1] liquibase : Creating database history table with name: PUBLIC.DATABASECHANGELOG 166 | $datetime INFO --- [ost-startStop-1] liquibase : Reading from PUBLIC.DATABASECHANGELOG 167 | $datetime INFO --- [ost-startStop-1] liquibase : classpath:/db/changelog/db.changelog-master.yaml: classpath:/db/changelog/db.changelog-master.yaml::1::andrejusti: Table Category created 168 | $datetime INFO --- [ost-startStop-1] liquibase : classpath:/db/changelog/db.changelog-master.yaml: classpath:/db/changelog/db.changelog-master.yaml::1::andrejusti: Table Product created 169 | $datetime INFO --- [ost-startStop-1] liquibase : classpath:/db/changelog/db.changelog-master.yaml: classpath:/db/changelog/db.changelog-master.yaml::1::andrejusti: Table Product_Category created 170 | $datetime INFO --- [ost-startStop-1] liquibase : classpath:/db/changelog/db.changelog-master.yaml: classpath:/db/changelog/db.changelog-master.yaml::1::andrejusti: ChangeSet classpath:/db/changelog/db.changelog-master.yaml::1::andrejusti ran successfully in 12ms 171 | $datetime INFO --- [ost-startStop-1] liquibase : Successfully released change log lock 172 | $datetime INFO --- [ost-startStop-1] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default' 173 | $datetime INFO --- [ost-startStop-1] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [ name: default ] 174 | $datetime INFO --- [ost-startStop-1] org.hibernate.Version : HHH000412: Hibernate Core {5.2.17.Final} 175 | $datetime INFO --- [ost-startStop-1] org.hibernate.cfg.Environment : HHH000206: hibernate.properties not found 176 | $datetime INFO --- [ost-startStop-1] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.0.1.Final} 177 | $datetime INFO --- [ost-startStop-1] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect 178 | $datetime INFO --- [ost-startStop-1] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' 179 | $datetime INFO --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Servlet dispatcherServlet mapped to [/] 180 | $datetime INFO --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Servlet simpleGraphQLServlet mapped to [/graphql/*] 181 | $datetime INFO --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Servlet webServlet mapped to [/h2-console/*] 182 | $datetime INFO --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*] 183 | $datetime INFO --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*] 184 | $datetime INFO --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*] 185 | $datetime INFO --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*] 186 | $datetime INFO --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'corsConfigurer' to: [/*] 187 | $datetime INFO --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 188 | $datetime INFO --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@32eebfca: startup date [Sun Sep 16 23:44:27 GMT 2018]; root of context hierarchy 189 | $datetime INFO --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse) 190 | $datetime INFO --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest) 191 | $datetime INFO --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/graphiql]}" onto public void com.oembedler.moon.graphiql.boot.GraphiQLController.graphiql(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse,java.util.Map) throws java.io.IOException 192 | $datetime INFO --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/voyager]}" onto public void com.oembedler.moon.voyager.boot.VoyagerController.voyager(javax.servlet.http.HttpServletResponse) throws java.io.IOException 193 | $datetime INFO --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 194 | $datetime INFO --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 195 | $datetime INFO --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup 196 | $datetime INFO --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Bean with name 'dataSource' has been autodetected for JMX exposure 197 | $datetime INFO --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Located MBean 'dataSource': registering with JMX server as MBean [com.zaxxer.hikari:name=dataSource,type=HikariDataSource] 198 | $datetime INFO --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 199 | $datetime INFO --- [ main] t.j.e.k.s.graphql.ApplicationKt : Started ApplicationKt in 11.396 seconds (JVM running for 12.04) 200 | $datetime INFO --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet' 201 | $datetime INFO --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started 202 | $datetime INFO --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 27 ms 203 | ``` 204 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | kotlinVersion = "1.2.60" 4 | springBootVersion = "2.0.4.RELEASE" 5 | } 6 | repositories { 7 | mavenCentral() 8 | gradlePluginPortal() 9 | } 10 | dependencies { 11 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 12 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") 13 | classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}") 14 | } 15 | } 16 | 17 | apply plugin: "kotlin" 18 | apply plugin: "kotlin-spring" 19 | apply plugin: "eclipse" 20 | apply plugin: "jacoco" 21 | apply plugin: "org.springframework.boot" 22 | apply plugin: "io.spring.dependency-management" 23 | 24 | group = "com.andrejusti" 25 | version = "0.0.1-SNAPSHOT" 26 | sourceCompatibility = 1.8 27 | 28 | jacoco { 29 | toolVersion = "0.8.2" 30 | } 31 | 32 | jacocoTestReport { 33 | group = "Reporting" 34 | reports { 35 | xml.enabled true 36 | html.enabled true 37 | csv.enabled true 38 | } 39 | afterEvaluate { 40 | classDirectories = files(classDirectories.files.collect { 41 | fileTree(dir: it, excludes: ["**/dto/**", "**/config/**", "**/*Application**"]) 42 | }) 43 | } 44 | } 45 | check.dependsOn jacocoTestReport 46 | 47 | test.finalizedBy(jacocoTestReport) 48 | 49 | compileKotlin { 50 | kotlinOptions { 51 | freeCompilerArgs = ["-Xjsr305=strict"] 52 | jvmTarget = "1.8" 53 | } 54 | } 55 | 56 | compileTestKotlin { 57 | kotlinOptions { 58 | freeCompilerArgs = ["-Xjsr305=strict"] 59 | jvmTarget = "1.8" 60 | } 61 | } 62 | 63 | repositories { 64 | mavenCentral() 65 | } 66 | 67 | dependencies { 68 | compile("org.springframework.boot:spring-boot-starter-data-jpa") 69 | compile("org.springframework.boot:spring-boot-starter-web") 70 | compile("com.fasterxml.jackson.module:jackson-module-kotlin") 71 | compile("org.liquibase:liquibase-core") 72 | compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8") 73 | compile("org.jetbrains.kotlin:kotlin-reflect") 74 | compile("org.apache.commons:commons-lang3:3.7") 75 | compile("com.graphql-java:graphql-java:9.2") 76 | compile("com.graphql-java:graphiql-spring-boot-starter:4.4.0") 77 | compile("com.graphql-java:graphql-java-tools:5.2.0") 78 | compile("com.graphql-java:graphql-spring-boot-starter:4.4.0") 79 | compile("com.graphql-java:graphql-java-servlet:5.0.0") 80 | compile("com.graphql-java:voyager-spring-boot-starter:4.4.0") 81 | runtime("com.h2database:h2") 82 | testCompile("org.springframework.boot:spring-boot-starter-test") 83 | } 84 | -------------------------------------------------------------------------------- /docs/images/graphiql.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justiandre/example-kotlin-springboot-graphql/e81a9c6daff1c2372cd27aecbe97545a4b7f24fc/docs/images/graphiql.gif -------------------------------------------------------------------------------- /docs/images/h2-console.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justiandre/example-kotlin-springboot-graphql/e81a9c6daff1c2372cd27aecbe97545a4b7f24fc/docs/images/h2-console.gif -------------------------------------------------------------------------------- /docs/images/voyager.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justiandre/example-kotlin-springboot-graphql/e81a9c6daff1c2372cd27aecbe97545a4b7f24fc/docs/images/voyager.gif -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justiandre/example-kotlin-springboot-graphql/e81a9c6daff1c2372cd27aecbe97545a4b7f24fc/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Feb 06 12:27:20 CET 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'example-kotlin-springboot-graphql' 2 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=justiandre_example-kotlin-springboot-graphql 2 | sonar.projectName=Projeto de exemplo com API GraphQL com dois crud’s simples, utilizando Kotlin, Spring Boot, Gradle etc 3 | sonar.projectVersion=0.0.1-SNAPSHOT 4 | 5 | # ===================================================== 6 | # Meta-data for the project 7 | # ===================================================== 8 | 9 | sonar.links.homepage=https://github.com/SonarSource/example-kotlin-springboot-graphql 10 | sonar.links.ci=https://travis-ci.org/SonarSource/example-kotlin-springboot-graphql 11 | sonar.links.scm=https://github.com/SonarSource/example-kotlin-springboot-graphql 12 | sonar.links.issue=https://github.com/SonarSource/example-kotlin-springboot-graphql/issues 13 | 14 | 15 | # ===================================================== 16 | # Properties that will be shared amongst all modules 17 | # ===================================================== 18 | 19 | # SQ standard properties 20 | sonar.sources=src/main 21 | sonar.tests=src/test -------------------------------------------------------------------------------- /src/main/kotlin/com/andrejusti/example/kotlin/springboot/graphql/Application.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | 6 | @SpringBootApplication 7 | class Application 8 | 9 | fun main(args: Array) { 10 | runApplication(*args) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/andrejusti/example/kotlin/springboot/graphql/domain/controller/graphql/config/CustomGraphQlErrorHandler.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.domain.controller.graphql.config 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore 4 | import graphql.ErrorType 5 | import graphql.ExceptionWhileDataFetching 6 | import graphql.GraphQLError 7 | import graphql.servlet.GraphQLErrorHandler 8 | import org.apache.commons.lang3.exception.ExceptionUtils 9 | import org.springframework.stereotype.Component 10 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.exception.ValidationException 11 | 12 | @Component 13 | class CustomGraphQlErrorHandler : GraphQLErrorHandler { 14 | 15 | companion object { 16 | const val KEY_ERRO_VALIDATION = "ItemValidationErrors" 17 | } 18 | 19 | override fun processErrors(errors: List): List { 20 | val erros = errors.filter { !isServerError(it) } 21 | val errosException = errors 22 | .filter { isServerError(it) } 23 | .map { parseGraphQLErrorAdapterException(it) } 24 | return erros + errosException 25 | } 26 | 27 | private fun isServerError(error: GraphQLError) = error is ExceptionWhileDataFetching 28 | 29 | private fun parseGraphQLErrorAdapterException(graphQLError: GraphQLError) = 30 | if (isValidationException(graphQLError)) 31 | GraphQLErrorAdapterExceptionValidation(graphQLError) 32 | else 33 | GraphQLErrorAdapterException(graphQLError) 34 | 35 | private fun isValidationException(graphQLError: GraphQLError) = 36 | (graphQLError as ExceptionWhileDataFetching).exception is ValidationException 37 | 38 | private open class GraphQLErrorAdapterException(@JsonIgnore val graphQLError: GraphQLError) : GraphQLError by graphQLError { 39 | 40 | override fun getMessage() = getExceptionWhileDataFetching().exception.message 41 | 42 | override fun getExtensions(): Map = 43 | getExceptionWhileDataFetching().exception 44 | .let { mapOf(it.javaClass.simpleName to ExceptionUtils.getRootCauseMessage(it)) } 45 | 46 | protected fun getExceptionWhileDataFetching() = graphQLError as ExceptionWhileDataFetching 47 | } 48 | 49 | private class GraphQLErrorAdapterExceptionValidation(@JsonIgnore val graphQLErrorValidation: GraphQLError) : GraphQLErrorAdapterException(graphQLErrorValidation) { 50 | 51 | override fun getExtensions() = mapOf(KEY_ERRO_VALIDATION to getValidationException().itemValidationErrors) 52 | 53 | override fun getErrorType() = ErrorType.ValidationError 54 | 55 | private fun getValidationException() = getExceptionWhileDataFetching().exception as ValidationException 56 | } 57 | } 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/main/kotlin/com/andrejusti/example/kotlin/springboot/graphql/domain/controller/graphql/resolver/CategoryGraphQlResolver.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.domain.controller.graphql.resolver 2 | 3 | import com.coxautodev.graphql.tools.GraphQLMutationResolver 4 | import com.coxautodev.graphql.tools.GraphQLQueryResolver 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.stereotype.Component 7 | import com.andrejusti.example.kotlin.springboot.graphql.domain.entity.Category 8 | import com.andrejusti.example.kotlin.springboot.graphql.domain.service.CategoryService 9 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.dto.Pagination 10 | 11 | @Component 12 | class CategoryGraphQlResolver( 13 | @Autowired val categoryService: CategoryService 14 | ) : GraphQLQueryResolver, GraphQLMutationResolver { 15 | 16 | fun category(id: Long) = categoryService.findById(id) 17 | 18 | fun categories(pagination: Pagination) = categoryService.findAll(pagination) 19 | 20 | fun createCategory(category: Category) = categoryService.save(category) 21 | 22 | fun editCategory(id: Long, category: Category) = categoryService.save(category.apply { this.id = id }) 23 | 24 | fun deleteCategory(id: Long) = categoryService.delete(id) 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/andrejusti/example/kotlin/springboot/graphql/domain/controller/graphql/resolver/ProductGraphQlResolver.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.domain.controller.graphql.resolver 2 | 3 | import com.coxautodev.graphql.tools.GraphQLMutationResolver 4 | import com.coxautodev.graphql.tools.GraphQLQueryResolver 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.stereotype.Component 7 | import com.andrejusti.example.kotlin.springboot.graphql.domain.entity.Category 8 | import com.andrejusti.example.kotlin.springboot.graphql.domain.entity.Product 9 | import com.andrejusti.example.kotlin.springboot.graphql.domain.service.ProductService 10 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.dto.Pagination 11 | 12 | @Component 13 | class ProductGraphQlResolver( 14 | @Autowired val productService: ProductService 15 | ) : GraphQLQueryResolver, GraphQLMutationResolver { 16 | 17 | fun product(id: Long) = productService.findById(id) 18 | 19 | fun products(pagination: Pagination, name: String?) = productService.findAllByName(pagination, name) 20 | 21 | fun createProduct(product: ProductInput) = productService.save(parseProduct(product)) 22 | 23 | fun editProduct(id: Long, product: ProductInput) = productService.save(parseProduct(product).apply { this.id = id }) 24 | 25 | fun deleteProduct(id: Long) = productService.delete(id) 26 | 27 | private fun parseProduct(productInput: ProductInput) = 28 | Product( 29 | name = productInput.name, 30 | description = productInput.description, 31 | value = productInput.value, 32 | categories = productInput.categories.map { Category(id = it) } 33 | ) 34 | 35 | object ProductInput { 36 | var name: String? = null 37 | var description: String? = null 38 | var value: Double? = null 39 | var categories: List = emptyList() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/com/andrejusti/example/kotlin/springboot/graphql/domain/entity/Category.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.domain.entity 2 | 3 | import javax.persistence.* 4 | 5 | @Entity 6 | data class Category( 7 | @Id 8 | @GeneratedValue(strategy = GenerationType.IDENTITY) 9 | var id: Long? = null, 10 | var name: String? = null 11 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/andrejusti/example/kotlin/springboot/graphql/domain/entity/Product.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.domain.entity 2 | 3 | import javax.persistence.* 4 | 5 | @Entity 6 | data class Product( 7 | @Id 8 | @GeneratedValue(strategy = GenerationType.IDENTITY) 9 | var id: Long? = null, 10 | var name: String? = null, 11 | var description: String? = null, 12 | var value: Double? = null, 13 | @ManyToMany(fetch = FetchType.EAGER) 14 | @JoinTable(name = "Product_Category", joinColumns = [JoinColumn(name = "product_id")], inverseJoinColumns = [JoinColumn(name = "category_id")]) 15 | var categories: List? = null 16 | ) 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/andrejusti/example/kotlin/springboot/graphql/domain/repository/api/CategoryRepository.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.domain.repository.api 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | import org.springframework.stereotype.Repository 5 | import com.andrejusti.example.kotlin.springboot.graphql.domain.entity.Category 6 | 7 | @Repository 8 | interface CategoryRepository : JpaRepository { 9 | 10 | fun existsByNameIgnoreCase(name: String?): Boolean 11 | 12 | fun existsByIdNotAndNameIgnoreCase(id: Long, name: String?): Boolean 13 | } 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/kotlin/com/andrejusti/example/kotlin/springboot/graphql/domain/repository/api/ProductRepository.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.domain.repository.api 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | import org.springframework.stereotype.Repository 5 | import com.andrejusti.example.kotlin.springboot.graphql.domain.entity.Product 6 | 7 | @Repository 8 | interface ProductRepository : JpaRepository { 9 | 10 | fun existsByCategoriesId(id: Long): Boolean 11 | 12 | fun existsByNameIgnoreCase(name: String?): Boolean 13 | 14 | fun existsByIdNotAndNameIgnoreCase(id: Long, name: String?): Boolean 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/andrejusti/example/kotlin/springboot/graphql/domain/service/CategoryService.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.domain.service 2 | 3 | import org.apache.commons.lang3.StringUtils 4 | import org.springframework.beans.factory.annotation.Autowired 5 | import org.springframework.stereotype.Service 6 | import org.springframework.transaction.annotation.Transactional 7 | import com.andrejusti.example.kotlin.springboot.graphql.domain.entity.Category 8 | import com.andrejusti.example.kotlin.springboot.graphql.domain.repository.api.CategoryRepository 9 | import com.andrejusti.example.kotlin.springboot.graphql.domain.repository.api.ProductRepository 10 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.dto.Pagination 11 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.service.PaginationService 12 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.service.ValidateService 13 | 14 | @Service 15 | class CategoryService( 16 | @Autowired val paginationService: PaginationService, 17 | @Autowired val validationService: ValidateService, 18 | @Autowired val categoryRepository: CategoryRepository, 19 | @Autowired val productRepository: ProductRepository 20 | ) { 21 | 22 | companion object { 23 | const val CATEGORY_NAME_MAX_SIZE = 70 24 | 25 | const val ITEM_VALIDATION_LOCATION_CATEGORY_NAME = "category.name" 26 | const val ITEM_VALIDATION_LOCATION_CATEGORY_PRODUCT = "category.product" 27 | 28 | const val ITEM_VALIDATION_ERROR_CATEGORY_NAME_NOT_BLACK = "category.name.notBlank" 29 | const val ITEM_VALIDATION_ERROR_CATEGORY_NAME_MAX_SIZE = "category.name.maxSize" 30 | const val ITEM_VALIDATION_ERROR_CATEGORY_DUPLICATE = "category.duplicate" 31 | const val ITEM_VALIDATION_ERROR_CATEGORY_RELATIONSHIP = "category.product.relationship" 32 | } 33 | 34 | fun findById(id: Long) = categoryRepository.findById(id).orElse(null) 35 | 36 | fun findAll(pagination: Pagination) = categoryRepository.findAll(paginationService.parsePagination(pagination)).content 37 | 38 | @Transactional 39 | fun save(category: Category) = category.let { 40 | validateSave(it) 41 | categoryRepository.save(it) 42 | } 43 | 44 | @Transactional 45 | fun delete(id: Long): Boolean { 46 | if (!categoryRepository.existsById(id)) { 47 | return false 48 | } 49 | validateDelete(id) 50 | categoryRepository.deleteById(id) 51 | return true 52 | } 53 | 54 | private fun validateDelete(idCategory: Long) = validationService.apply { 55 | addIfItemConditionIsTrue(productRepository.existsByCategoriesId(idCategory), ITEM_VALIDATION_LOCATION_CATEGORY_PRODUCT, ITEM_VALIDATION_ERROR_CATEGORY_RELATIONSHIP) 56 | }.validate() 57 | 58 | private fun validateSave(category: Category) = validationService.apply { 59 | addIfItemConditionIsTrue(StringUtils.isBlank(category.name), ITEM_VALIDATION_LOCATION_CATEGORY_NAME, ITEM_VALIDATION_ERROR_CATEGORY_NAME_NOT_BLACK) 60 | addIfItemConditionIsTrueAndNotHasError({ StringUtils.length(category.name) > CATEGORY_NAME_MAX_SIZE }, ITEM_VALIDATION_LOCATION_CATEGORY_NAME, ITEM_VALIDATION_ERROR_CATEGORY_NAME_MAX_SIZE) 61 | addIfItemConditionIsTrueAndNotHasError({ isDuplicateCategory(category) }, ITEM_VALIDATION_LOCATION_CATEGORY_NAME, ITEM_VALIDATION_ERROR_CATEGORY_DUPLICATE) 62 | }.validate() 63 | 64 | private fun isDuplicateCategory(category: Category) = 65 | category.id?.let { 66 | categoryRepository.existsByIdNotAndNameIgnoreCase(it, category.name) 67 | } ?: categoryRepository.existsByNameIgnoreCase(category.name) 68 | } 69 | -------------------------------------------------------------------------------- /src/main/kotlin/com/andrejusti/example/kotlin/springboot/graphql/domain/service/ProductService.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.domain.service 2 | 3 | import org.apache.commons.lang3.StringUtils 4 | import org.apache.commons.lang3.math.NumberUtils 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.data.domain.Example 7 | import org.springframework.stereotype.Service 8 | import org.springframework.transaction.annotation.Transactional 9 | import org.springframework.util.CollectionUtils 10 | import com.andrejusti.example.kotlin.springboot.graphql.domain.entity.Product 11 | import com.andrejusti.example.kotlin.springboot.graphql.domain.repository.api.ProductRepository 12 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.dto.Pagination 13 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.service.PaginationService 14 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.service.ValidateService 15 | 16 | @Service 17 | class ProductService( 18 | @Autowired val paginationService: PaginationService, 19 | @Autowired val validationService: ValidateService, 20 | @Autowired val productRepository: ProductRepository 21 | ) { 22 | 23 | companion object { 24 | const val PRODUCT_NAME_MAX_SIZE = 70 25 | const val PRODUCT_DESCRIPTION_MAX_SIZE = 4000 26 | 27 | const val ITEM_VALIDATION_LOCATION_PRODUCT_NAME = "product.name" 28 | const val ITEM_VALIDATION_LOCATION_PRODUCT_DESCRIPTION = "product.description" 29 | const val ITEM_VALIDATION_LOCATION_PRODUCT_VALUE = "product.value" 30 | const val ITEM_VALIDATION_LOCATION_PRODUCT_CATEGORY = "product.category" 31 | 32 | const val ITEM_VALIDATION_ERROR_PRODUCT_NAME_NOT_BLACK = "product.name.notBlank" 33 | const val ITEM_VALIDATION_ERROR_PRODUCT_NAME_MAX_SIZE = "product.name.maxSize" 34 | const val ITEM_VALIDATION_ERROR_PRODUCT_DESCRIPTION_MAX_SIZE = "product.description.maxSize" 35 | const val ITEM_VALIDATION_ERROR_PRODUCT_VALUE_NOT_NEGATIVE = "product.value.notNegative" 36 | const val ITEM_VALIDATION_ERROR_PRODUCT_CATEGORY_REQUIRED = "product.category.required" 37 | const val ITEM_VALIDATION_ERROR_PRODUCT_DUPLICATE = "product.duplicate" 38 | } 39 | 40 | fun findById(id: Long) = productRepository.findById(id).orElse(null) 41 | 42 | fun findAllByName(pagination: Pagination, name: String?): List { 43 | val filter = Example.of(Product(name = StringUtils.trimToNull(name))) 44 | val paginationNormalized = paginationService.parsePagination(pagination) 45 | return productRepository.findAll(filter, paginationNormalized).content 46 | } 47 | 48 | @Transactional 49 | fun save(product: Product) = product.let { 50 | validateSave(it) 51 | productRepository.save(it) 52 | } 53 | 54 | @Transactional 55 | fun delete(id: Long): Boolean { 56 | if (!productRepository.existsById(id)) { 57 | return false 58 | } 59 | productRepository.deleteById(id) 60 | return true 61 | } 62 | 63 | private fun validateSave(product: Product) = validationService.apply { 64 | addIfItemConditionIsTrue(StringUtils.isBlank(product.name), ITEM_VALIDATION_LOCATION_PRODUCT_NAME, ITEM_VALIDATION_ERROR_PRODUCT_NAME_NOT_BLACK) 65 | addIfItemConditionIsTrueAndNotHasError({ StringUtils.length(product.name) > PRODUCT_NAME_MAX_SIZE }, ITEM_VALIDATION_LOCATION_PRODUCT_NAME, ITEM_VALIDATION_ERROR_PRODUCT_NAME_MAX_SIZE) 66 | addIfItemConditionIsTrueAndNotHasError({ isDuplicateProduct(product) }, ITEM_VALIDATION_LOCATION_PRODUCT_NAME, ITEM_VALIDATION_ERROR_PRODUCT_DUPLICATE) 67 | addIfItemConditionIsTrue(StringUtils.length(product.description) > PRODUCT_DESCRIPTION_MAX_SIZE, ITEM_VALIDATION_LOCATION_PRODUCT_DESCRIPTION, ITEM_VALIDATION_ERROR_PRODUCT_DESCRIPTION_MAX_SIZE) 68 | addIfItemConditionIsTrue(CollectionUtils.isEmpty(product.categories), ITEM_VALIDATION_LOCATION_PRODUCT_CATEGORY, ITEM_VALIDATION_ERROR_PRODUCT_CATEGORY_REQUIRED) 69 | product.value?.apply { 70 | addIfItemConditionIsTrue(this <= NumberUtils.DOUBLE_ZERO, ITEM_VALIDATION_LOCATION_PRODUCT_VALUE, ITEM_VALIDATION_ERROR_PRODUCT_VALUE_NOT_NEGATIVE) 71 | } 72 | }.validate() 73 | 74 | fun isDuplicateProduct(product: Product) = 75 | product.id?.let { 76 | productRepository.existsByIdNotAndNameIgnoreCase(it, product.name) 77 | } ?: productRepository.existsByNameIgnoreCase(product.name) 78 | } 79 | 80 | -------------------------------------------------------------------------------- /src/main/kotlin/com/andrejusti/example/kotlin/springboot/graphql/infrastructure/dto/ItemValidationError.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.infrastructure.dto 2 | 3 | data class ItemValidationError( 4 | var errorLocation: String, 5 | var messageKey: String, 6 | var context: Map? = null 7 | ) 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/andrejusti/example/kotlin/springboot/graphql/infrastructure/dto/Pagination.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.infrastructure.dto 2 | 3 | data class Pagination( 4 | var page: Int? = null, 5 | var maxRecords: Int? = null 6 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/andrejusti/example/kotlin/springboot/graphql/infrastructure/exception/ValidationException.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.infrastructure.exception 2 | 3 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.dto.ItemValidationError 4 | 5 | data class ValidationException(val itemValidationErrors: List) : RuntimeException() -------------------------------------------------------------------------------- /src/main/kotlin/com/andrejusti/example/kotlin/springboot/graphql/infrastructure/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * O intuito desse pacote é manter as classes responsáveis pela infraestrutura da aplicação ou classes utilitárias, 3 | * o fato é que esse pacote não deveria existir, o tal código visto aqui, deveria estar em componentes isolados fora 4 | * da aplicação, podendo ser reaproveitado também em outras aplicações e fazendo um isolamento do codigo de negócio 5 | * do codigo de infraestrutura ou utilitários. 6 | */ 7 | 8 | package com.andrejusti.example.kotlin.springboot.graphql.infrastructure; -------------------------------------------------------------------------------- /src/main/kotlin/com/andrejusti/example/kotlin/springboot/graphql/infrastructure/service/PaginationService.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.infrastructure.service 2 | 3 | import org.apache.commons.lang3.math.NumberUtils 4 | import org.springframework.beans.factory.annotation.Value 5 | import org.springframework.data.domain.PageRequest 6 | import org.springframework.data.domain.Pageable 7 | import org.springframework.stereotype.Service 8 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.dto.Pagination 9 | 10 | @Service 11 | class PaginationService( 12 | @Value("\${app.search.pagination.numberRecords.default}") val paginationNumberRecordsDefault: Int, 13 | @Value("\${app.search.pagination.numberRecords.max}") val paginationNumberRecordsMax: Int 14 | ) { 15 | 16 | fun parsePagination(pagination: Pagination) = parsePagination(pagination.page, pagination.maxRecords) 17 | 18 | fun parsePagination(page: Int?, maxRecords: Int?): Pageable { 19 | val pageNormalized = page ?: NumberUtils.INTEGER_ZERO 20 | val maxRecordsNormalized: Int = (maxRecords ?: paginationNumberRecordsDefault) 21 | .takeIf { paginationNumberRecordsMax > it } 22 | ?: paginationNumberRecordsMax 23 | return PageRequest.of(pageNormalized, maxRecordsNormalized) 24 | } 25 | } 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/kotlin/com/andrejusti/example/kotlin/springboot/graphql/infrastructure/service/ValidateService.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.infrastructure.service 2 | 3 | import org.springframework.stereotype.Service 4 | import org.springframework.web.context.annotation.RequestScope 5 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.dto.ItemValidationError 6 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.exception.ValidationException 7 | 8 | @RequestScope 9 | @Service 10 | class ValidateService { 11 | 12 | private var itemValidationErrors: ArrayList = arrayListOf() 13 | 14 | fun hasValidationError() = itemValidationErrors.isNotEmpty() 15 | 16 | fun addItemValidation(itemValidationError: ItemValidationError) { 17 | itemValidationError.apply { itemValidationErrors.add(this) } 18 | } 19 | 20 | fun addIfItemConditionIsTrue(condition: () -> Boolean, itemValidationError: ItemValidationError) { 21 | if (condition()) { 22 | addItemValidation(itemValidationError) 23 | } 24 | } 25 | 26 | fun addIfItemConditionIsTrue(condition: () -> Boolean, errorLocation: String, messageKey: String, context: Map? = null) { 27 | addIfItemConditionIsTrue(condition, ItemValidationError(errorLocation, messageKey, context)) 28 | } 29 | 30 | fun addIfItemConditionIsTrue(condition: Boolean, itemValidationError: ItemValidationError) { 31 | addIfItemConditionIsTrue({ condition }, itemValidationError) 32 | } 33 | 34 | fun addIfItemConditionIsTrue(condition: Boolean, errorLocation: String, messageKey: String, context: Map? = null) { 35 | addIfItemConditionIsTrue(condition, ItemValidationError(errorLocation, messageKey, context)) 36 | } 37 | 38 | 39 | fun addIfItemConditionIsTrueAndNotHasError(condition: () -> Boolean, errorLocation: String, messageKey: String, context: Map? = null) { 40 | if (!hasValidationError()) { 41 | addIfItemConditionIsTrue(condition, errorLocation, messageKey, context) 42 | } 43 | } 44 | 45 | fun validate() { 46 | if (hasValidationError()) { 47 | throw ValidationException(itemValidationErrors) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | app: 2 | search: 3 | pagination: 4 | numberRecords: 5 | default: 10 6 | max: 100 7 | 8 | spring: 9 | application.name: exemplo-kotlin-springboot-graphql 10 | h2.console: 11 | enabled: true 12 | path: /h2-console 13 | jpa.hibernate.ddl-auto: validate 14 | 15 | # Configuração de log. 16 | logging: 17 | level: 18 | org: 19 | apache.http: INFO 20 | springframework: INFO 21 | spring: INFO 22 | hibernate: 23 | SQL: DEBUG 24 | type.descriptor.sql.BasicBinder: TRACE 25 | com: 26 | andrejustiandre: INFO 27 | andrejusti.example.kotlin.springboot.graphql.infrastructure.exception.ValidationException: OFF 28 | -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | 2 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 3 | + __ __ + 4 | + /\ \ __ /\ \__ __ + 5 | + __ ___ \_\ \ _ __ __ /\_\ __ __ ___\ \ ,_\/\_\ ___ ___ ___ ___ + 6 | + /'__`\ /' _ `\ /'_` \/\`'__\/'__`\\/\ \/\ \/\ \ /',__\ \ \/\/\ \ /'___\ / __`\ /' __` __`\ + 7 | + /\ \L\.\_/\ \/\ \/\ \L\ \ \ \//\ __/ \ \ \ \ \_\ \/\__, `\ \ \_\ \ \ __/\ \__//\ \L\ \/\ \/\ \/\ \ + 8 | + \ \__/.\_\ \_\ \_\ \___,_\ \_\\ \____\_\ \ \ \____/\/\____/\ \__\\ \_\/\_\ \____\ \____/\ \_\ \_\ \_\ + 9 | + \/__/\/_/\/_/\/_/\/__,_ /\/_/ \/____/\ \_\ \/___/ \/___/ \/__/ \/_/\/_/\/____/\/___/ \/_/\/_/\/_/ + 10 | + \ \____/ + 11 | + \/___/ + 12 | + + 13 | + App de exemplo: Kotlin + Spring Boot + GraphQL + 14 | + https://github.com/justiandre/example-kotlin-springboot-graphql + 15 | + Por: André Justi - http://andrejusti.com + 16 | + + 17 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 18 | -------------------------------------------------------------------------------- /src/main/resources/db/changelog/db.changelog-master.yaml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - changeSet: 3 | id: 1 4 | author: andrejusti 5 | changes: 6 | - createTable: 7 | tableName: Category 8 | columns: 9 | - column: 10 | name: id 11 | type: long 12 | autoIncrement: true 13 | constraints: 14 | primaryKey: true 15 | nullable: false 16 | - column: 17 | name: name 18 | type: varchar(70) 19 | constraints: 20 | nullable: false 21 | - createTable: 22 | tableName: Product 23 | columns: 24 | - column: 25 | name: id 26 | type: long 27 | autoIncrement: true 28 | constraints: 29 | primaryKey: true 30 | nullable: false 31 | - column: 32 | name: name 33 | type: varchar(70) 34 | constraints: 35 | nullable: false 36 | - column: 37 | name: description 38 | type: varchar(4000) 39 | - column: 40 | name: value 41 | type: double 42 | - createTable: 43 | tableName: Product_Category 44 | columns: 45 | - column: 46 | name: product_id 47 | type: long 48 | constraints: 49 | nullable: false 50 | - column: 51 | name: category_id 52 | type: long 53 | constraints: 54 | nullable: false 55 | -------------------------------------------------------------------------------- /src/main/resources/graphql/inputs.graphqls: -------------------------------------------------------------------------------- 1 | input Pagination { 2 | page: Int 3 | maxRecords: Int 4 | } 5 | 6 | input CategoryInput { 7 | name: String! 8 | } 9 | 10 | input ProductInput { 11 | name: String! 12 | description: String 13 | value: Float 14 | categories: [Int!] 15 | } -------------------------------------------------------------------------------- /src/main/resources/graphql/mutations.graphqls: -------------------------------------------------------------------------------- 1 | type Mutation { 2 | 3 | # Criar categoria 4 | createCategory(category: CategoryInput!): Category 5 | 6 | # Editar categoria 7 | editCategory(id: Int!, category: CategoryInput!): Category 8 | 9 | # Remover categoria 10 | deleteCategory(id: Int!): Boolean 11 | 12 | # Criar produto 13 | createProduct(product: ProductInput!): Product 14 | 15 | # Editar produto 16 | editProduct(id: Int!, product: ProductInput!): Product 17 | 18 | # Remover produto 19 | deleteProduct(id: Int!): Boolean 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/graphql/querys.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | 3 | # Obter categoria por identificador 4 | category(id: Int!): Category 5 | 6 | # Obter categorias de maneira paginada 7 | categories(pagination: Pagination): [Category!] 8 | 9 | # Obter product por identificador 10 | product(id: Int!): Product 11 | 12 | # Obter produtos 13 | products(pagination: Pagination, name: String): [Product!] 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/main/resources/graphql/types.graphqls: -------------------------------------------------------------------------------- 1 | type Product { 2 | id: ID! 3 | name: String 4 | description: String 5 | value: Float 6 | categories: [Category!] 7 | } 8 | 9 | type Category { 10 | id: ID! 11 | name: String 12 | } 13 | -------------------------------------------------------------------------------- /src/test/kotlin/com/andrejusti/example/kotlin/springboot/graphql/domain/AbstractIT.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.domain 2 | 3 | import org.apache.commons.lang3.math.NumberUtils 4 | import org.junit.Assert 5 | import org.junit.FixMethodOrder 6 | import org.junit.runner.RunWith 7 | import org.junit.runners.MethodSorters 8 | import org.springframework.boot.test.context.SpringBootTest 9 | import org.springframework.test.context.junit4.SpringRunner 10 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.dto.Pagination 11 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.exception.ValidationException 12 | import java.util.* 13 | 14 | @RunWith(SpringRunner::class) 15 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 16 | abstract class AbstractIT { 17 | 18 | protected fun createRandomValue() = UUID.randomUUID().toString() 19 | 20 | protected fun createPagination() = Pagination(page = NumberUtils.INTEGER_ZERO, maxRecords = Int.MAX_VALUE) 21 | 22 | protected fun assertValidationException(messageKey: String, exec: () -> Unit) { 23 | try { 24 | exec() 25 | Assert.fail("Not generated validation exception") 26 | } catch (validationException: ValidationException) { 27 | Assert.assertTrue("Invalid validation exception", validationException.itemValidationErrors.any { it.messageKey == messageKey }) 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/andrejusti/example/kotlin/springboot/graphql/domain/it/CategoryIT.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.domain.it 2 | 3 | import org.apache.commons.lang3.StringUtils 4 | import org.apache.commons.lang3.math.NumberUtils 5 | import org.junit.Assert 6 | import org.junit.Ignore 7 | import org.junit.Test 8 | import org.springframework.beans.factory.annotation.Autowired 9 | import com.andrejusti.example.kotlin.springboot.graphql.domain.AbstractIT 10 | import com.andrejusti.example.kotlin.springboot.graphql.domain.entity.Category 11 | import com.andrejusti.example.kotlin.springboot.graphql.domain.entity.Product 12 | import com.andrejusti.example.kotlin.springboot.graphql.domain.sdk.CategorySdk 13 | import com.andrejusti.example.kotlin.springboot.graphql.domain.sdk.ProductSdk 14 | import com.andrejusti.example.kotlin.springboot.graphql.domain.service.CategoryService 15 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.dto.Pagination 16 | 17 | class CategoryIT : AbstractIT() { 18 | 19 | @Autowired 20 | lateinit var productSdk: ProductSdk 21 | 22 | @Autowired 23 | lateinit var categorySdk: CategorySdk 24 | 25 | @Test 26 | fun `Search category by id without expecting result`() { 27 | val category = categorySdk.category(Int.MAX_VALUE.toLong()) 28 | Assert.assertNull("Should not return category", category) 29 | } 30 | 31 | @Test 32 | fun `Search categories without expecting result`() { 33 | val categories = categorySdk.categories(createPagination()) 34 | Assert.assertNotNull("Should not return null", categories) 35 | } 36 | 37 | @Test 38 | fun `Delete nonexistent category`() { 39 | val hasDeleted = categorySdk.deleteCategory(Int.MAX_VALUE.toLong()) 40 | Assert.assertFalse("Should not remove category", hasDeleted) 41 | } 42 | 43 | @Test 44 | fun `Create category checking id`() { 45 | val category = createAndSaveCategory() 46 | Assert.assertNotNull("Should return id", category.id) 47 | } 48 | 49 | @Test 50 | fun `Delete category checking find by id`() { 51 | assertDeleteCategory(categorySdk::category) 52 | } 53 | 54 | @Test 55 | fun `Delete category checking find all`() { 56 | assertDeleteCategory(::findAllCategoryId) 57 | } 58 | 59 | @Test 60 | fun `Delete category checking validation - category product relationship`() { 61 | val category = createAndSaveCategory() 62 | productSdk.createProduct(Product(name = createRandomValue(), categories = listOf(category))) 63 | val execValidationException: () -> Unit = { categorySdk.deleteCategory(category.id!!) } 64 | assertValidationException(CategoryService.ITEM_VALIDATION_ERROR_CATEGORY_RELATIONSHIP, execValidationException) 65 | } 66 | 67 | @Test 68 | fun `Create category checking find by id`() { 69 | assertCreateCategory(categorySdk::category) 70 | } 71 | 72 | @Test 73 | fun `Create category checking find all`() { 74 | assertCreateCategory(::findAllCategoryId) 75 | } 76 | 77 | @Test 78 | fun `Edit category checking find by id`() { 79 | assertEditCategory(categorySdk::category) 80 | } 81 | 82 | @Test 83 | fun `Edit category checking find all`() { 84 | assertEditCategory(::findAllCategoryId) 85 | } 86 | 87 | @Test 88 | fun `Create category checking validation - category duplicate`() { 89 | val category = createCategory() 90 | categorySdk.createCategory(category) 91 | assertValidationException(CategoryService.ITEM_VALIDATION_ERROR_CATEGORY_DUPLICATE, { categorySdk.createCategory(category) }) 92 | } 93 | 94 | @Test 95 | fun `Edit category checking validation - category duplicate`() { 96 | val category1 = createAndSaveCategory() 97 | val category2 = createAndSaveCategory() 98 | category1.name = category2.name 99 | assertValidationException(CategoryService.ITEM_VALIDATION_ERROR_CATEGORY_DUPLICATE, { categorySdk.editCategory(category1) }) 100 | } 101 | 102 | @Test 103 | fun `Create category checking validation - category name not black`() { 104 | assertValidationExceptionCategoryNameNotBlack(createCategory(), { categorySdk.createCategory(it) }) 105 | } 106 | 107 | @Test 108 | fun `Edit category checking validation - category name not black`() { 109 | assertValidationExceptionCategoryNameNotBlack(createAndSaveCategory(), { categorySdk.editCategory(it) }) 110 | } 111 | 112 | @Test 113 | fun `Create category checking validation - category name max size`() { 114 | assertValidationExceptionCategoryNameMaxSize(createCategory(), { categorySdk.createCategory(it) }) 115 | } 116 | 117 | @Test 118 | fun `Edit category checking validation - category name max size`() { 119 | assertValidationExceptionCategoryNameMaxSize(createAndSaveCategory(), { categorySdk.editCategory(it) }) 120 | } 121 | 122 | @Test 123 | fun `Search find all without informing pagination`() { 124 | categorySdk.categories(Pagination()) 125 | } 126 | 127 | private fun findAllCategoryId(categoryId: Long) = categorySdk.categories(createPagination()).firstOrNull { it.id == categoryId } 128 | 129 | private fun assertDeleteCategory(searchCategory: (Long) -> Category?) { 130 | val category = assertCreateCategory(searchCategory) 131 | categorySdk.deleteCategory(category.id!!) 132 | val categorySearchAfterDelete = searchCategory(category.id!!) 133 | Assert.assertNull("Should not return to category after deleting", categorySearchAfterDelete) 134 | } 135 | 136 | private fun assertEditCategory(searchCategory: (Long) -> Category?) { 137 | val category = assertCreateCategory(searchCategory) 138 | category.name = createRandomValue() 139 | categorySdk.editCategory(category) 140 | val categorySearch = searchCategory(category.id!!) 141 | Assert.assertEquals("Category retrieved is different from edited", category, categorySearch) 142 | } 143 | 144 | private fun assertCreateCategory(searchCategory: (Long) -> Category?): Category { 145 | val category = createAndSaveCategory() 146 | val categoryId = category.id 147 | Assert.assertNotNull("Should return id", categoryId) 148 | val categorySearch = searchCategory(categoryId!!) 149 | Assert.assertEquals("Category retrieved is different from saved", category, categorySearch) 150 | return categorySearch!! 151 | } 152 | 153 | private fun assertValidationExceptionCategoryNameNotBlack(category: Category, execValidationException: (Category) -> Unit) { 154 | category.name = StringUtils.EMPTY 155 | val exec = { execValidationException(category) } 156 | assertValidationException(CategoryService.ITEM_VALIDATION_ERROR_CATEGORY_NAME_NOT_BLACK, exec) 157 | } 158 | 159 | private fun assertValidationExceptionCategoryNameMaxSize(category: Category, execValidationException: (Category) -> Unit) { 160 | category.name = StringUtils.repeat("A", CategoryService.CATEGORY_NAME_MAX_SIZE + NumberUtils.INTEGER_ONE) 161 | val exec = { execValidationException(category) } 162 | assertValidationException(CategoryService.ITEM_VALIDATION_ERROR_CATEGORY_NAME_MAX_SIZE, exec) 163 | } 164 | 165 | private fun createAndSaveCategory() = categorySdk.createCategory(createCategory()) 166 | 167 | private fun createCategory() = Category(name = createRandomValue()) 168 | } 169 | 170 | -------------------------------------------------------------------------------- /src/test/kotlin/com/andrejusti/example/kotlin/springboot/graphql/domain/it/ProductIT.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.domain.it 2 | 3 | import org.apache.commons.lang3.StringUtils 4 | import org.apache.commons.lang3.math.NumberUtils 5 | import org.junit.Assert 6 | import org.junit.Test 7 | import org.springframework.beans.factory.annotation.Autowired 8 | import com.andrejusti.example.kotlin.springboot.graphql.domain.AbstractIT 9 | import com.andrejusti.example.kotlin.springboot.graphql.domain.entity.Category 10 | import com.andrejusti.example.kotlin.springboot.graphql.domain.entity.Product 11 | import com.andrejusti.example.kotlin.springboot.graphql.domain.sdk.CategorySdk 12 | import com.andrejusti.example.kotlin.springboot.graphql.domain.sdk.ProductSdk 13 | import com.andrejusti.example.kotlin.springboot.graphql.domain.service.ProductService 14 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.dto.Pagination 15 | 16 | class ProductIT : AbstractIT() { 17 | 18 | @Autowired 19 | lateinit var productSdk: ProductSdk 20 | 21 | @Autowired 22 | lateinit var categorySdk: CategorySdk 23 | 24 | @Test 25 | fun `Search product by id without expecting result`() { 26 | val product = productSdk.product(Int.MAX_VALUE.toLong()) 27 | Assert.assertNull("Should not return product", product) 28 | } 29 | 30 | @Test 31 | fun `Search products without expecting result`() { 32 | val products = productSdk.products(createPagination(), createRandomValue()) 33 | Assert.assertNotNull("Should not return null", products) 34 | } 35 | 36 | @Test 37 | fun `Search products by name expecting results`() { 38 | val product = createAndSaveProduct() 39 | val productSearch = productSdk.products(createPagination(), product.name!!).firstOrNull { it.id == product.id } 40 | Assert.assertEquals("Product retrieved is different from saved", product, productSearch) 41 | } 42 | 43 | @Test 44 | fun `Delete nonexistent product`() { 45 | val hasDeleted = productSdk.deleteProduct(Int.MAX_VALUE.toLong()) 46 | Assert.assertFalse("Should not remove product", hasDeleted) 47 | } 48 | 49 | @Test 50 | fun `Create product checking id`() { 51 | val product = createAndSaveProduct() 52 | Assert.assertNotNull("Should return id", product.id) 53 | } 54 | 55 | @Test 56 | fun `Delete product checking find by id`() { 57 | assertDeleteProduct(productSdk::product) 58 | } 59 | 60 | @Test 61 | fun `Delete product checking find all`() { 62 | assertDeleteProduct(::findAllProductId) 63 | } 64 | 65 | @Test 66 | fun `Create product checking find by id`() { 67 | assertCreateProduct(productSdk::product) 68 | } 69 | 70 | @Test 71 | fun `Create product checking find all`() { 72 | assertCreateProduct(::findAllProductId) 73 | } 74 | 75 | @Test 76 | fun `Edit product checking find by id`() { 77 | assertEditProduct(productSdk::product) 78 | } 79 | 80 | @Test 81 | fun `Edit product checking find all`() { 82 | assertEditProduct(::findAllProductId) 83 | } 84 | 85 | @Test 86 | fun `Create product checking validation - product duplicate`() { 87 | val product = createProduct() 88 | productSdk.createProduct(product) 89 | assertValidationException(ProductService.ITEM_VALIDATION_ERROR_PRODUCT_DUPLICATE, { productSdk.createProduct(product) }) 90 | } 91 | 92 | @Test 93 | fun `Edit product checking validation - product duplicate`() { 94 | val product1 = createAndSaveProduct() 95 | val product2 = createAndSaveProduct() 96 | product1.name = product2.name 97 | assertValidationException(ProductService.ITEM_VALIDATION_ERROR_PRODUCT_DUPLICATE, { productSdk.editProduct(product1) }) 98 | } 99 | 100 | @Test 101 | fun `Create product checking validation - product name not black`() { 102 | assertValidationExceptionProductNameNotBlack(createProduct(), { productSdk.createProduct(it) }) 103 | } 104 | 105 | @Test 106 | fun `Edit product checking validation - product name not black`() { 107 | assertValidationExceptionProductNameNotBlack(createAndSaveProduct(), { productSdk.editProduct(it) }) 108 | } 109 | 110 | @Test 111 | fun `Create product checking validation - product name max size`() { 112 | assertValidationExceptionProductNameMaxSize(createProduct(), { productSdk.createProduct(it) }) 113 | } 114 | 115 | @Test 116 | fun `Edit product checking validation - product name max size`() { 117 | assertValidationExceptionProductNameMaxSize(createAndSaveProduct(), { productSdk.editProduct(it) }) 118 | } 119 | 120 | @Test 121 | fun `Create product checking validation - product description max size`() { 122 | assertValidationExceptionProductDescriptionMaxSize(createProduct(), { productSdk.createProduct(it) }) 123 | } 124 | 125 | @Test 126 | fun `Edit product checking validation - product description max size`() { 127 | assertValidationExceptionProductDescriptionMaxSize(createAndSaveProduct(), { productSdk.editProduct(it) }) 128 | } 129 | 130 | @Test 131 | fun `Create product checking validation - product value not negative`() { 132 | assertValidationExceptionProductValueNotNegative(createProduct(), { productSdk.createProduct(it) }) 133 | } 134 | 135 | @Test 136 | fun `Edit product checking validation - product value not negative`() { 137 | assertValidationExceptionProductValueNotNegative(createAndSaveProduct(), { productSdk.editProduct(it) }) 138 | } 139 | 140 | @Test 141 | fun `Create product checking validation - product category required`() { 142 | assertValidationExceptionProductCategoryRequired(createProduct(), { productSdk.createProduct(it) }) 143 | } 144 | 145 | @Test 146 | fun `Edit product checking validation - product category required`() { 147 | assertValidationExceptionProductCategoryRequired(createAndSaveProduct(), { productSdk.editProduct(it) }) 148 | } 149 | 150 | @Test 151 | fun `Search find all without informing pagination`() { 152 | productSdk.products(Pagination(), StringUtils.EMPTY) 153 | } 154 | 155 | private fun findAllProductId(productId: Long) = productSdk.products(createPagination(), StringUtils.EMPTY).firstOrNull { it.id == productId } 156 | 157 | private fun createAndSaveCategory() = categorySdk.createCategory(createCategory()) 158 | 159 | private fun createCategory() = Category(name = createRandomValue()) 160 | 161 | private fun createAndSaveProduct() = productSdk.createProduct(createProduct()) 162 | 163 | private fun createProduct() = Product( 164 | name = createRandomValue(), 165 | description = createRandomValue(), 166 | value = NumberUtils.DOUBLE_ONE, 167 | categories = listOf(createAndSaveCategory()) 168 | ) 169 | 170 | private fun assertDeleteProduct(searchProduct: (Long) -> Product?) { 171 | val product = assertCreateProduct(searchProduct) 172 | productSdk.deleteProduct(product.id!!) 173 | val productSearchAfterDelete = searchProduct(product.id!!) 174 | Assert.assertNull("Should not return to product after deleting", productSearchAfterDelete) 175 | } 176 | 177 | private fun assertEditProduct(searchProduct: (Long) -> Product?) { 178 | val product = assertCreateProduct(searchProduct) 179 | product.name = createRandomValue() 180 | productSdk.editProduct(product) 181 | val productSearch = searchProduct(product.id!!) 182 | Assert.assertEquals("Product retrieved is different from edited", product, productSearch) 183 | } 184 | 185 | private fun assertCreateProduct(searchProduct: (Long) -> Product?): Product { 186 | val product = createAndSaveProduct() 187 | val productId = product.id 188 | Assert.assertNotNull("Should return id", productId) 189 | val productSearch = searchProduct(productId!!) 190 | Assert.assertEquals("Product retrieved is different from saved", product, productSearch) 191 | return productSearch!! 192 | } 193 | 194 | private fun assertValidationExceptionProductNameNotBlack(product: Product, execValidationException: (Product) -> Unit) { 195 | product.name = StringUtils.EMPTY 196 | val exec = { execValidationException(product) } 197 | assertValidationException(ProductService.ITEM_VALIDATION_ERROR_PRODUCT_NAME_NOT_BLACK, exec) 198 | } 199 | 200 | private fun assertValidationExceptionProductNameMaxSize(product: Product, execValidationException: (Product) -> Unit) { 201 | product.name = StringUtils.repeat("A", ProductService.PRODUCT_NAME_MAX_SIZE + NumberUtils.INTEGER_ONE) 202 | val exec = { execValidationException(product) } 203 | assertValidationException(ProductService.ITEM_VALIDATION_ERROR_PRODUCT_NAME_MAX_SIZE, exec) 204 | } 205 | 206 | private fun assertValidationExceptionProductDescriptionMaxSize(product: Product, execValidationException: (Product) -> Unit) { 207 | product.description = StringUtils.repeat("A", ProductService.PRODUCT_DESCRIPTION_MAX_SIZE + NumberUtils.INTEGER_ONE) 208 | val exec = { execValidationException(product) } 209 | assertValidationException(ProductService.ITEM_VALIDATION_ERROR_PRODUCT_DESCRIPTION_MAX_SIZE, exec) 210 | } 211 | 212 | private fun assertValidationExceptionProductValueNotNegative(product: Product, execValidationException: (Product) -> Unit) { 213 | product.value = NumberUtils.DOUBLE_MINUS_ONE 214 | val exec = { execValidationException(product) } 215 | assertValidationException(ProductService.ITEM_VALIDATION_ERROR_PRODUCT_VALUE_NOT_NEGATIVE, exec) 216 | } 217 | 218 | private fun assertValidationExceptionProductCategoryRequired(product: Product, execValidationException: (Product) -> Unit) { 219 | product.categories = null 220 | val exec = { execValidationException(product) } 221 | assertValidationException(ProductService.ITEM_VALIDATION_ERROR_PRODUCT_CATEGORY_REQUIRED, exec) 222 | } 223 | } 224 | 225 | -------------------------------------------------------------------------------- /src/test/kotlin/com/andrejusti/example/kotlin/springboot/graphql/domain/sdk/AbstractSdk.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.domain.sdk 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference 4 | import com.fasterxml.jackson.databind.JsonNode 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import graphql.ErrorType 7 | import org.apache.commons.lang3.StringUtils 8 | import org.springframework.beans.factory.annotation.Autowired 9 | import org.springframework.beans.factory.annotation.Value 10 | import org.springframework.boot.test.web.client.TestRestTemplate 11 | import org.springframework.stereotype.Component 12 | import com.andrejusti.example.kotlin.springboot.graphql.domain.controller.graphql.config.CustomGraphQlErrorHandler 13 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.dto.ItemValidationError 14 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.exception.ValidationException 15 | 16 | @Component 17 | abstract class AbstractSdk { 18 | 19 | @Autowired 20 | protected lateinit var testRestTemplate: TestRestTemplate 21 | 22 | @Value("\${graphql.servlet.mapping:/graphql}") 23 | protected lateinit var graphQLPath: String 24 | 25 | @Autowired 26 | protected lateinit var objectMapperJson: ObjectMapper 27 | 28 | final inline fun execGraphQl(body: String?): T { 29 | val response = execGraphQlRequest(body) 30 | val responseJson = objectMapperJson.reader().readTree(response) 31 | val erros = responseJson.get("errors") 32 | if (StringUtils.isNotBlank(erros?.toString())) { 33 | throwErroResponseGraphQL(erros) 34 | } 35 | val responseSucess = responseJson?.get("data")?.get("response").toString() 36 | return objectMapperJson.reader().forType(object : TypeReference() {}).readValue(responseSucess) 37 | } 38 | 39 | fun throwErroResponseGraphQL(jsonNode: JsonNode) { 40 | val erros = jsonNode.toList() 41 | val validationError = erros 42 | .filter { StringUtils.equalsIgnoreCase(it.get("errorType")?.asText(), ErrorType.ValidationError.name) } 43 | .first() 44 | validationError 45 | ?.let { throwValidationErrorItensResponseGraphQL(validationError) } 46 | ?: throw RuntimeException("Unexpected error in response errors block: [bodyResponseErros: '${jsonNode.toString()}']") 47 | } 48 | 49 | fun throwValidationErrorItensResponseGraphQL(validationError: JsonNode) { 50 | val validationErrorItens = validationError.get("extensions")?.get(CustomGraphQlErrorHandler.KEY_ERRO_VALIDATION)?.toList() 51 | val validationErrorItensNormalized = objectMapperJson.reader().forType(object : TypeReference>() {}).readValue>(validationErrorItens?.toString()) 52 | throw ValidationException(validationErrorItensNormalized) 53 | } 54 | 55 | fun execGraphQlRequest(body: String?): String? { 56 | val bodyRequestNormalized = body 57 | ?.let { StringUtils.replaceAll(it, "\n|\r|\t", StringUtils.SPACE) } 58 | ?.let { StringUtils.replaceAll(it, "(\\s{1,})", StringUtils.SPACE) } 59 | val response = testRestTemplate.postForEntity(graphQLPath, bodyRequestNormalized, String::class.java) 60 | val responseBody = response?.body 61 | if (response.statusCode.is2xxSuccessful) { 62 | return responseBody 63 | } 64 | throw RuntimeException("Unexpected error while executing request graphql: [statusHttp: '${response.statusCode}', bodyResponse: '$responseBody']") 65 | } 66 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/andrejusti/example/kotlin/springboot/graphql/domain/sdk/CategorySdk.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.domain.sdk 2 | 3 | import org.springframework.stereotype.Component 4 | import com.andrejusti.example.kotlin.springboot.graphql.domain.entity.Category 5 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.dto.Pagination 6 | 7 | @Component 8 | class CategorySdk : AbstractSdk() { 9 | 10 | fun category(id: Long): Category? { 11 | val query = """ 12 | { 13 | "query":"query { 14 | response: category(id: $id) { 15 | id 16 | name 17 | } 18 | }", 19 | "variables":null 20 | } 21 | """ 22 | return execGraphQl(query) 23 | } 24 | 25 | fun categories(pagination: Pagination): List { 26 | val query = """ 27 | { 28 | "query":"query { 29 | response: categories(pagination: {page: ${pagination.page}, maxRecords: ${pagination.maxRecords}}) { 30 | id 31 | name 32 | } 33 | }", 34 | "variables":null 35 | } 36 | """ 37 | return execGraphQl>(query) 38 | } 39 | 40 | fun createCategory(category: Category): Category { 41 | val mutation = """ 42 | { 43 | "query":"mutation createCategory { 44 | response: createCategory(category: {name: \"${category.name}\" }) { 45 | id 46 | name 47 | } 48 | }", 49 | "variables":null 50 | } 51 | """ 52 | return execGraphQl(mutation) 53 | } 54 | 55 | fun editCategory(category: Category): Category { 56 | val mutation = """ 57 | { 58 | "query":"mutation editCategory { 59 | response: editCategory(id: ${category.id}, category: {name: \"${category.name}\"}) { 60 | id 61 | name 62 | } 63 | }", 64 | "variables":null 65 | } 66 | """ 67 | return execGraphQl(mutation) 68 | } 69 | 70 | fun deleteCategory(id: Long): Boolean { 71 | val mutation = """ 72 | { 73 | "query":"mutation deleteCategory { 74 | response: deleteCategory(id: $id) 75 | }", 76 | "variables":null 77 | } 78 | """ 79 | return execGraphQl(mutation) 80 | } 81 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/andrejusti/example/kotlin/springboot/graphql/domain/sdk/ProductSdk.kt: -------------------------------------------------------------------------------- 1 | package com.andrejusti.example.kotlin.springboot.graphql.domain.sdk 2 | 3 | import org.apache.commons.lang3.StringUtils 4 | import org.springframework.stereotype.Component 5 | import com.andrejusti.example.kotlin.springboot.graphql.domain.entity.Category 6 | import com.andrejusti.example.kotlin.springboot.graphql.domain.entity.Product 7 | import com.andrejusti.example.kotlin.springboot.graphql.infrastructure.dto.Pagination 8 | 9 | @Component 10 | class ProductSdk : AbstractSdk() { 11 | 12 | fun product(id: Long): Product? { 13 | val query = """ 14 | { 15 | "query":"query { 16 | response: product(id: $id) { 17 | id 18 | name 19 | value 20 | description 21 | categories { id } 22 | } 23 | }", 24 | "variables":null 25 | } 26 | """ 27 | return execGraphQl(query) 28 | } 29 | 30 | fun products(pagination: Pagination, name: String): List { 31 | val query = """ 32 | { 33 | "query":"query { 34 | response: products(pagination: {page: ${pagination.page}, maxRecords: ${pagination.maxRecords}}, name: \"$name\") { 35 | id 36 | name 37 | value 38 | description 39 | categories { id } 40 | } 41 | }", 42 | "variables":null 43 | } 44 | """ 45 | return execGraphQl>(query) 46 | } 47 | 48 | fun createProduct(product: Product): Product { 49 | val mutation = """ 50 | { 51 | "query":"mutation createProduct { 52 | response: createProduct(product: {name: \"${product.name}\", description: \"${product.description}\", value: ${product.value}, categories: [${normalizeCategory(product)}]}) { 53 | id 54 | name 55 | value 56 | description 57 | categories { id } 58 | } 59 | }", 60 | "variables":null 61 | } 62 | """ 63 | return execGraphQl(mutation) 64 | } 65 | 66 | fun editProduct(product: Product): Product { 67 | val mutation = """ 68 | { 69 | "query":"mutation editProduct { 70 | response: editProduct(id: ${product.id}, product: {name: \"${product.name}\", description: \"${product.description}\", value: ${product.value}, categories: [${normalizeCategory(product)}]}) { 71 | id 72 | name 73 | value 74 | description 75 | categories { id } 76 | } 77 | }", 78 | "variables":null 79 | } 80 | """ 81 | return execGraphQl(mutation) 82 | } 83 | 84 | fun deleteProduct(id: Long): Boolean { 85 | val mutation = """ 86 | { 87 | "query":"mutation deleteProduct { 88 | response: deleteProduct(id: $id) 89 | }", 90 | "variables":null 91 | } 92 | """ 93 | return execGraphQl(mutation) 94 | } 95 | 96 | private fun normalizeCategory(product: Product?) = 97 | product 98 | ?.categories 99 | ?.map { it.id } 100 | ?.filterNotNull() 101 | ?.joinToString(separator = ", ") 102 | ?: StringUtils.EMPTY 103 | } --------------------------------------------------------------------------------