├── .gitattributes ├── .github └── workflows │ └── gradle.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── docs ├── .nojekyll ├── README.md ├── _sidebar.md ├── codegen │ ├── advanced-configuration.md │ ├── code-generation-with-spring-boot-integration.md │ ├── gettings-started.md │ └── schema-configuration.md ├── index.html └── spring-boot-integration │ ├── annotations.md │ └── getting-started.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── graphql-kotlin-toolkit-codegen-binding ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── com │ └── auritylab │ └── graphql │ └── kotlin │ └── toolkit │ └── codegenbinding │ └── types │ ├── AbstractEnv.kt │ ├── MetaField.kt │ ├── MetaFieldWithReference.kt │ ├── MetaFieldsContainer.kt │ ├── MetaInterfaceType.kt │ ├── MetaObjectType.kt │ └── Value.kt ├── graphql-kotlin-toolkit-codegen ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── auritylab │ │ └── graphql │ │ └── kotlin │ │ └── toolkit │ │ └── codegen │ │ ├── Codegen.kt │ │ ├── CodegenController.kt │ │ ├── CodegenOptions.kt │ │ ├── CodegenSchemaParser.kt │ │ ├── codeblock │ │ └── ArgumentCodeBlockGenerator.kt │ │ ├── generator │ │ ├── AbstractClassGenerator.kt │ │ ├── AbstractInputDataClassGenerator.kt │ │ ├── EnumGenerator.kt │ │ ├── FileGenerator.kt │ │ ├── GeneratorFactory.kt │ │ ├── InputObjectGenerator.kt │ │ ├── ObjectTypeGenerator.kt │ │ ├── fieldResolver │ │ │ ├── AbstractFieldResolverGenerator.kt │ │ │ ├── FieldResolverGenerator.kt │ │ │ └── PaginationFieldResolverGenerator.kt │ │ ├── meta │ │ │ └── MetaFieldsContainerGenerator.kt │ │ └── pagination │ │ │ ├── PaginationConnectionGenerator.kt │ │ │ ├── PaginationEdgeGenerator.kt │ │ │ ├── PaginationInfoGenerator.kt │ │ │ └── PaginationPageInfoGenerator.kt │ │ ├── helper │ │ ├── GraphQLNameHelper.kt │ │ ├── GraphQLWrapTypeHelper.kt │ │ ├── NamingHelper.kt │ │ └── SpringBootIntegrationHelper.kt │ │ └── mapper │ │ ├── BindingMapper.kt │ │ ├── GeneratedMapper.kt │ │ ├── ImplementerMapper.kt │ │ └── KotlinTypeMapper.kt │ └── test │ ├── kotlin │ └── com │ │ └── auritylab │ │ └── graphql │ │ └── kotlin │ │ └── toolkit │ │ └── codegen │ │ ├── CodegenSchemaParserTest.kt │ │ ├── _test │ │ ├── AbstractCompilationTest.kt │ │ ├── TestObject.kt │ │ └── TestUtils.kt │ │ ├── codeblock │ │ └── ArgumentCodeBlockGeneratorTest.kt │ │ ├── generator │ │ ├── EnumGeneratorTest.kt │ │ ├── InputObjectGeneratorTest.kt │ │ ├── ObjectTypeGeneratorTest.kt │ │ ├── fieldResolver │ │ │ └── FieldResolverGeneratorTest.kt │ │ └── meta │ │ │ └── MetaFieldsContainerGeneratorTest.kt │ │ └── mapper │ │ └── KotlinTypeMapperTest.kt │ └── resources │ └── testschema.graphqls ├── graphql-kotlin-toolkit-common ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── auritylab │ │ └── graphql │ │ └── kotlin │ │ └── toolkit │ │ └── common │ │ ├── directive │ │ ├── AbstractDirective.kt │ │ ├── Directive.kt │ │ ├── DirectiveFacade.kt │ │ ├── HasArgumentsDirective.kt │ │ ├── exception │ │ │ └── DirectiveValidationException.kt │ │ └── implementation │ │ │ ├── DoubleNullDirective.kt │ │ │ ├── GenerateDirective.kt │ │ │ ├── PaginationDirective.kt │ │ │ ├── RepresentationDirective.kt │ │ │ └── ResolverDirective.kt │ │ ├── helper │ │ ├── GraphQLEqualityHelper.kt │ │ └── GraphQLTypeHelper.kt │ │ └── markers │ │ └── Experimental.kt │ └── test │ ├── kotlin │ └── com │ │ └── auritylab │ │ └── graphql │ │ └── kotlin │ │ └── toolkit │ │ └── common │ │ ├── directive │ │ ├── AbstractDirectiveTest.kt │ │ └── DirectiveFacadeTest.kt │ │ └── helper │ │ ├── GraphQLEqualityHelperTest.kt │ │ └── GraphQLTypeHelperTest.kt │ └── resources │ └── allDirectives.graphqls ├── graphql-kotlin-toolkit-gradle-plugin ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── com │ └── auritylab │ └── graphql │ └── kotlin │ └── toolkit │ └── gradle │ ├── CodegenGradlePlugin.kt │ ├── extension │ └── CodegenExtension.kt │ └── task │ └── CodegenTask.kt ├── graphql-kotlin-toolkit-spring-boot ├── build.gradle.kts └── src │ ├── main │ ├── kotlin │ │ └── com │ │ │ └── auritylab │ │ │ └── graphql │ │ │ └── kotlin │ │ │ └── toolkit │ │ │ └── spring │ │ │ ├── AutoConfiguration.kt │ │ │ ├── annotation │ │ │ ├── AnnotationResolver.kt │ │ │ ├── GQLDirective.kt │ │ │ ├── GQLResolver.kt │ │ │ ├── GQLResolvers.kt │ │ │ ├── GQLScalar.kt │ │ │ └── GQLTypeResolver.kt │ │ │ ├── api │ │ │ ├── GraphQLInvocation.kt │ │ │ ├── GraphQLSchemaSupplier.kt │ │ │ └── GraphQLSchemaSupplierExtensions.kt │ │ │ ├── configuration │ │ │ ├── GraphQLConfiguration.kt │ │ │ ├── GraphQLProperties.kt │ │ │ ├── InstrumentationConfiguration.kt │ │ │ └── SchemaConfiguration.kt │ │ │ ├── controller │ │ │ ├── AbstractController.kt │ │ │ ├── GetController.kt │ │ │ ├── PostController.kt │ │ │ └── UploadController.kt │ │ │ ├── internal │ │ │ ├── InternalGQLInvocation.kt │ │ │ └── InternalWiringFactory.kt │ │ │ ├── provided │ │ │ └── ProvidedScalars.kt │ │ │ └── schema │ │ │ ├── BaseSchemaAugmentation.kt │ │ │ ├── SchemaAugmentation.kt │ │ │ ├── SchemaTypeGenerator.kt │ │ │ └── pagination │ │ │ ├── PaginationPageInfoTypeGenerator.kt │ │ │ ├── PaginationSchemaAugmentation.kt │ │ │ └── PaginationTypesGenerator.kt │ └── resources │ │ └── META-INF │ │ └── spring.factories │ └── test │ ├── kotlin │ └── com │ │ └── auritylab │ │ └── graphql │ │ └── kotlin │ │ └── toolkit │ │ └── spring │ │ ├── SyncGQLInvocation.kt │ │ ├── TestConfiguration.kt │ │ ├── TestOperations.kt │ │ ├── api │ │ └── GraphQLSchemaSupplierExtensionsTest.kt │ │ └── controller │ │ ├── AbstractControllerTest.kt │ │ ├── AbstractDataFetcherControllerTest.kt │ │ ├── AbstractInvocationControllerTest.kt │ │ ├── GetControllerTest.kt │ │ ├── PostControllerTest.kt │ │ ├── UploadControllerTest.kt │ │ └── UploadDataFetcherControllerTest.kt │ └── resources │ ├── schemas │ └── schema.graphqls │ └── test_file.png ├── graphql-kotlin-toolkit-util ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── auritylab │ │ └── graphql │ │ └── kotlin │ │ └── toolkit │ │ └── util │ │ └── selection │ │ ├── EnhancedDataFetchingFieldSelectionSet.kt │ │ ├── SelectionSetExtensions.kt │ │ └── steps │ │ ├── SelectionSetStepImpls.kt │ │ └── SelectionSetSteps.kt │ └── test │ ├── kotlin │ └── com │ │ └── auritylab │ │ └── graphql │ │ └── kotlin │ │ └── toolkit │ │ └── util │ │ └── jpa │ │ └── _TestUtils.kt │ └── resources │ ├── .graphqlconfig │ ├── query.graphql │ └── schema.graphqls ├── scripts └── release.sh └── settings.gradle.kts /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Gradle 2 | 3 | on: [ push ] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | java_version: [ 11, 15 ] 11 | fail-fast: false 12 | steps: 13 | - uses: actions/checkout@v1 14 | 15 | # Configure the Java version based on the current matrix value. 16 | - uses: actions/setup-java@v1 17 | with: 18 | java-version: ${{ matrix.java_version }} 19 | 20 | # Build the entire project (includes unit tests) 21 | - name: Build 22 | uses: burrunan/gradle-cache-action@v1 23 | with: 24 | job-id: ${{ matrix.java_version }} 25 | arguments: build 26 | 27 | # Merge the JaCoCo execution data from all modules and create one report for all. 28 | - name: Jacoco merge 29 | uses: burrunan/gradle-cache-action@v1 30 | with: 31 | job-id: ${{ matrix.java_version }} 32 | arguments: jacocoMergeReport 33 | execution-only-caches: true 34 | 35 | # Upload the JaCoCo report to CodeCov. 36 | - name: CodeCov upload 37 | uses: codecov/codecov-action@v1 38 | with: 39 | token: ${{ secrets.CODECOV_TOKEN }} 40 | files: ./build/reports/jacoco/jacocoMergeReport/jacocoMergeReport.xml 41 | flags: jacoco,unittest,matrix-java-version-${{ matrix.java_version }} 42 | 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/** 6 | !**/src/test/** 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | out/ 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | 35 | ### Additional 36 | .local/ 37 | spec_failures 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Kotlin Toolkit 2 | [![GitHub Actions](https://github.com/AurityLab/graphql-kotlin-toolkit/workflows/Gradle/badge.svg)](https://github.com/AurityLab/graphql-kotlin-toolkit/actions) 3 | [![codecov](https://codecov.io/gh/AurityLab/graphql-kotlin-toolkit/branch/master/graph/badge.svg?token=e8c5dSYCAS)](https://codecov.io/gh/AurityLab/graphql-kotlin-toolkit) 4 | [![Maven Central](https://img.shields.io/maven-central/v/com.auritylab.graphql-kotlin-toolkit/codegen?label=codegen)](https://mvnrepository.com/artifact/com.auritylab.graphql-kotlin-toolkit/codegen) 5 | [![Maven Central](https://img.shields.io/maven-central/v/com.auritylab.graphql-kotlin-toolkit/spring-boot?label=spring%20boot%20integration)](https://mvnrepository.com/artifact/com.auritylab.graphql-kotlin-toolkit/spring-boot) 6 | 7 | A toolkit for GraphQL, specifically for [Kotlin](https://kotlinlang.org/). This toolkit provides some useful tools that are compatible with [graphql-java](https://github.com/graphql-java/graphql-java). 8 | 9 | ## Code generation 10 | This tool follows the **schema-first** approach, in which you first write your *schema.graphqls* files and implement the server-side code for it afterwards. 11 | This code generator additionally creates an interface for each resolver. 12 | These can be used to implement each resolver in a clean way. The tool also provides specific parameters for each argument, allowing a more type safe way to access the incoming data. 13 | This code generator also **supports Kotlin's null safety feature**! 14 | 15 | Example resolver: 16 | ```kotlin 17 | class MutationUpdateUser : GQLMutationUpdateUser { 18 | override fun resolve(input: GQLUpdateUserInput, env: GQLMutationUpdateUser.Env): User { 19 | TODO("implement your resolver") 20 | } 21 | } 22 | ``` 23 | 24 | **Getting started [here](docs/codegen/gettings-started.md)!** 25 | 26 | 27 | ## Spring Boot integration 28 | This integration works in a more opinionated way as it provides additional annotations which can be used to register code for various GraphQL types. 29 | It also comes with a servlet, which handles all GraphQL requests. 30 | 31 | **Getting started [here](docs/spring-boot-integration/getting-started.md)!** 32 | 33 | 34 | ## Documentation 35 | * Code generation 36 | * [Getting started (Gradle Plugin)](docs/codegen/gettings-started.md) 37 | * [Schema configuration](docs/codegen/schema-configuration.md) 38 | * [Advanced configuration (Gradle Plugin)](docs/codegen/advanced-configuration.md) 39 | * [Code generation with Spring Boot integration](docs/codegen/code-generation-with-spring-boot-integration.md) 40 | * Spring Boot integration 41 | * [Getting started](docs/spring-boot-integration/getting-started.md) 42 | * [Annotations](docs/spring-boot-integration/annotations.md) 43 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AurityLab/graphql-kotlin-toolkit/3dbb4e3b43cf713da34297bf827b63caaeb15ffa/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Kotlin Toolkit 2 | [![GitHub Actions](https://github.com/AurityLab/graphql-kotlin-toolkit/workflows/Gradle/badge.svg)](https://github.com/AurityLab/graphql-kotlin-toolkit/actions) 3 | [![Maven Central](https://img.shields.io/maven-central/v/com.auritylab.graphql-kotlin-toolkit/codegen?label=codegen)](https://mvnrepository.com/artifact/com.auritylab.graphql-kotlin-toolkit/codegen) 4 | [![Maven Central](https://img.shields.io/maven-central/v/com.auritylab.graphql-kotlin-toolkit/spring-boot?label=spring%20boot%20integration)](https://mvnrepository.com/artifact/com.auritylab.graphql-kotlin-toolkit/spring-boot) 5 | 6 | A toolkit for GraphQL, specifically for [Kotlin](https://kotlinlang.org/). This toolkit provides some useful tools that are compatible with [graphql-java](https://github.com/graphql-java/graphql-java). 7 | 8 | ## Code generation 9 | This tool follows the **schema-first** approach, in which you first write your *schema.graphqls* files and implement the server-side code for it afterwards. 10 | This code generator additionally creates an interface for each resolver. 11 | These can be used to implement each resolver in a clean way. The tool also provides specific parameters for each argument, allowing a more type safe way to access the incoming data. 12 | This code generator also **supports Kotlin's null safety feature**! 13 | 14 | Example resolver: 15 | ```kotlin 16 | class MutationUpdateUser : GQLMutationUpdateUser { 17 | override fun resolve(input: GQLUpdateUserInput, env: GQLMutationUpdateUser.Env): User { 18 | TODO("implement your resolver") 19 | } 20 | } 21 | ``` 22 | 23 | **Getting started [here](/codegen/gettings-started.md)!** 24 | 25 | 26 | ## Spring Boot integration 27 | This integration works in a more opinionated way as it provides additional annotations which can be used to register code for various GraphQL types. 28 | It also comes with a servlet, which handles all GraphQL requests. 29 | 30 | **Getting started [here](/spring-boot-integration/getting-started.md)!** 31 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * Code generation 2 | * [Getting started (Gradle Plugin)](/codegen/gettings-started.md) 3 | * [Schema configuration](/codegen/schema-configuration.md) 4 | * [Advanced configuration (Gradle Plugin)](/codegen/advanced-configuration.md) 5 | * [Code generation with Spring Boot integration](/codegen/code-generation-with-spring-boot-integration.md) 6 | * Spring Boot integration 7 | * [Getting started](/spring-boot-integration/getting-started.md) 8 | * [Annotations](/spring-boot-integration/annotations.md) 9 | 10 | -------------------------------------------------------------------------------- /docs/codegen/advanced-configuration.md: -------------------------------------------------------------------------------- 1 | # Advanced configuration (Gradle Plugin) 2 | There are some advanced configuration properties for the Gradle Plugin. 3 | 4 | | Property | Type | Default | Description | 5 | |----------|------|----------|------------| 6 | | `schemas` | File collection | **(required)** | Defines the schemas for the code generation. | 7 | | `outputDirectory` | Directory | *"generated/graphql/kotlin/main/"* | Defines the output directory for the generated code. | 8 | | `generatedGlobalPrefix` | String? | *null* | Defines the global prefix for all generated types. | 9 | | `generatedBasePackage` | String | *"graphql.kotlin.toolkit.codegen"* | Defines the base package for the generated code. 10 | | `generateAll` | Boolean | *true* | Defines if the code generator shall generate code for all types. ([**See here for more information**](schema-configuration.md)) 11 | | `enableSpringBootIntegration` | Boolean | *false* | Defines if the code generator shall generate code which can simplify usage with the Spring Boot Integration ([**See here for more information**](code-generation-with-spring-boot-integration.md)) 12 | -------------------------------------------------------------------------------- /docs/codegen/code-generation-with-spring-boot-integration.md: -------------------------------------------------------------------------------- 1 | # Code generation with Spring Boot integration 2 | This code generation can be configured to generate code which can simplify usage with the Spring Boot integration from this toolkit. 3 | 4 | ### Usage 5 | To enable it add the following property to the Gradle Plugin configuration: 6 | ```kotlin 7 | graphqlKotlinCodegen { 8 | // ... 9 | enableSpringBootIntegration.set(true) 10 | // ... 11 | } 12 | ``` 13 | 14 | ### Effects on the generated code 15 | The code generator will add the [GQLResolver](../spring-boot-integration/annotations.md#GQLResolver) annotation to the generated resolvers. 16 | 17 | To be more precise take a look at the following example: 18 | ```kotlin 19 | @GQLResolver(CONTAINER, FIELD) // <- This annotation will be added. 20 | abstract class GQLQueryGetUser : DataFetcher { 21 | abstract fun resolve(env: GQLEnv): Any 22 | 23 | override fun get(env: DataFetchingEnvironment): Any { 24 | val map = env.arguments 25 | return resolve(env = GQLEnv(env)) 26 | } 27 | 28 | companion object Meta { 29 | const val CONTAINER: String = "Query" 30 | 31 | const val FIELD: String = "getUser" 32 | } 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/codegen/gettings-started.md: -------------------------------------------------------------------------------- 1 | # Getting started (Gradle Plugin) 2 | The code generation can easily be used with the provided Gradle plugin. 3 | The plugin will automatically hook into your `build` task and generate the code before it. 4 | It also creates the required sourceset for the generated code by itself. 5 | 6 | ### Plugin setup 7 | ```kotlin 8 | plugins { 9 | // Apply the plugin with the latest version. 10 | id("com.auritylab.graphql-kotlin-toolkit.codegen") version "0.5.0" 11 | } 12 | 13 | // Configure the code generation. 14 | graphqlKotlinCodegen { 15 | // Define your schemas. 16 | schemas.from(fileTree("src/main/resources/graphql").matching { include("*.graphqls") }) 17 | } 18 | ``` 19 | 20 | After you've configured the plugin you may [**continue with configuring your schema**](/codegen/schema-configuration.md). 21 | 22 | Further configuration options for the plugin can be found [here](/codegen/advanced-configuration.md). 23 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GraphQL Kotlin Toolkit 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/spring-boot-integration/annotations.md: -------------------------------------------------------------------------------- 1 | # Annotations 2 | You can register your code through the following annotations which are provided by this integration. 3 | 4 | #### [**GQLResolver**](https://github.com/AurityLab/graphql-kotlin-toolkit/blob/master/spring/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/annotation/GQLResolver.kt) 5 | Defines a resolver using the given container name and field name. 6 | 7 | Example: 8 | ```kotlin 9 | @GQLResolver("Query", "getMe") 10 | class QueryGetUserResolver : DataFetcher { 11 | override fun get(env: DataFetchingEnvironment): Any = TODO("implement") 12 | } 13 | ``` 14 | 15 | #### [**GQLTypeResolver**](https://github.com/AurityLab/graphql-kotlin-toolkit/blob/master/spring/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/annotation/GQLTypeResolver.kt) 16 | Defines a new TypeResolver for the given type name and scope. 17 | 18 | Example: 19 | ```kotlin 20 | @GQLTypeResolver("User", GQLTypeResolver.Scope.INTERFACE) 21 | class UserTypeResolver : TypeResolver { 22 | override fun getType(env: TypeResolutionEnvironment?): GraphQLObjectType = TODO("implement") 23 | } 24 | ``` 25 | 26 | #### [**GQLScalar**](https://github.com/AurityLab/graphql-kotlin-toolkit/blob/master/spring/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/annotation/GQLScalar.kt) 27 | Defines a new Scalar for the given scalar name. 28 | 29 | Example: 30 | ```kotlin 31 | @GQLScalar("NewString") 32 | class NewStringCoercing : Coercing { 33 | override fun parseValue(input: Any): String = TODO("implement") 34 | override fun parseLiteral(input: Any?): String = TODO("implement") 35 | override fun serialize(dataFetcherResult: Any): String = TODO("implement") 36 | } 37 | ``` 38 | 39 | #### [**GQLDirective**](https://github.com/AurityLab/graphql-kotlin-toolkit/blob/master/spring/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/annotation/GQLDirective.kt) 40 | Defines a new Directive for the given directive name. 41 | 42 | Example: 43 | ```kotlin 44 | @GQLDirective("authentication") 45 | class AuthenticationDirective : SchemaDirectiveWiring { 46 | override fun onField(env: SchemaDirectiveWiringEnvironment): GraphQLFieldDefinition = TODO("implement") 47 | } 48 | 49 | ``` 50 | 51 | #### [**GQLResolvers**](https://github.com/AurityLab/graphql-kotlin-toolkit/blob/master/spring/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/annotation/GQLResolvers.kt) 52 | Defines multiple resolvers for the given [GQLResolver](#GQLResolver). 53 | 54 | Example: 55 | ```kotlin 56 | @GQLResolvers( 57 | GQLResolver("PrivateUser", "age"), 58 | GQLResolver("PublicUser", "age") 59 | ) 60 | class UserAgeResolver : DataFetcher { 61 | override fun get(env: DataFetchingEnvironment): Any = TODO("implement") 62 | } 63 | ``` 64 | -------------------------------------------------------------------------------- /docs/spring-boot-integration/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | The Spring Boot starter configures most parts by it self. You just have to configure your GraphQL schemas. 3 | 4 | ## Dependency 5 | #### Gradle 6 | ```kotlin 7 | dependencies { 8 | implementation("com.auritylab.graphql-kotlin-toolkit:spring-boot-starter:0.5.0") 9 | } 10 | ``` 11 | 12 | #### Maven 13 | ```xml 14 | 15 | com.auritylab.graphql-kotlin-toolkit 16 | spring-boot-starter 17 | 0.5.0 18 | 19 | ``` 20 | 21 | ## Schemas 22 | After adding the dependencies you need to tell the integration where to search for your schemas. 23 | All you have to do is to add a Bean of type [`GQLSchemaSupplier`](../../graphql-kotlin-toolkit-spring/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/configuration/GQLSchemaConfiguration.kt). 24 | 25 | 26 | Using schema files which are located in your resources folder: 27 | ```kotlin 28 | @Configuration 29 | class GraphQLConfiguration { 30 | @Bean 31 | fun schemaSupplier() = GQLSchemaSupplier.ofResourceFiles( 32 | "graphql/schema.graphqls" 33 | // Other schema files 34 | ) 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel=true 2 | org.gradle.vfs.watch=true 3 | org.gradle.caching=true 4 | systemProp.org.gradle.internal.publish.checksums.insecure=true 5 | kotlin.code.style=official 6 | kotlin.parallel.tasks.in.project=true 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AurityLab/graphql-kotlin-toolkit/3dbb4e3b43cf713da34297bf827b63caaeb15ffa/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | distributionSha256Sum=f581709a9c35e9cb92e16f585d2c4bc99b2b1a5f85d2badbd3dc6bff59e1e6dd 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen-binding/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.kapt") 3 | } 4 | 5 | ext { 6 | this["publication.enabled"] = true 7 | this["publication.artifactId"] = "codegen-binding" 8 | this["publication.name"] = "GraphQL Kotlin Toolkit: Codegen binding" 9 | this["publication.description"] = "Binding for Codegen" 10 | } 11 | 12 | dependencies { 13 | implementation(project(":graphql-kotlin-toolkit-common")) 14 | 15 | // GraphQL-Java dependency. 16 | implementation("com.graphql-java:graphql-java:16.2") 17 | 18 | // Test dependencies. 19 | testImplementation("com.github.VerachadW:kraph:v.0.6.1") 20 | testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0") 21 | } 22 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen-binding/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegenbinding/types/AbstractEnv.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegenbinding.types 2 | 3 | import graphql.schema.DataFetchingEnvironment 4 | 5 | /** 6 | * Abstract environment for resolvers. This just requires the original [DataFetchingEnvironment] to resolve other values. 7 | * 8 | * @param P Type of the parent of this resolver. 9 | * @param C Type of the global context. 10 | * @param original The original [DataFetchingEnvironment] provided by graphql-java. 11 | */ 12 | @Suppress("unused") 13 | abstract class AbstractEnv

( 14 | @Suppress("CanBeParameter") val original: DataFetchingEnvironment, 15 | val type: T 16 | ) { 17 | /** 18 | * Provides the parent of this resolver. 19 | */ 20 | val parent: P 21 | get() = original.getSource() 22 | 23 | /** 24 | * Provides the global context. 25 | */ 26 | val context: C 27 | get() = original.getContext() 28 | } 29 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen-binding/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegenbinding/types/MetaField.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegenbinding.types 2 | 3 | import kotlin.reflect.KClass 4 | 5 | /** 6 | * @param R Type of the runtime type of this field. 7 | */ 8 | @Suppress("unused") 9 | interface MetaField { 10 | /** 11 | * Returns the actual name of the field. 12 | */ 13 | val name: String 14 | 15 | /** 16 | * Returns the type of the field as a string. 17 | */ 18 | val type: String 19 | 20 | /** 21 | * Returns the type of the field as a [KClass]. This might be [Any] if the type could not be determined. 22 | */ 23 | val runtimeType: KClass 24 | } 25 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen-binding/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegenbinding/types/MetaFieldWithReference.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegenbinding.types 2 | 3 | /** 4 | * @param T Type of the referenced type of this field. 5 | * @param R Type of the runtime type of this field. 6 | */ 7 | @Suppress("unused") 8 | interface MetaFieldWithReference : MetaField { 9 | /** 10 | * The reference to the meta information object of the type of this field. 11 | */ 12 | val ref: T 13 | } 14 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen-binding/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegenbinding/types/MetaFieldsContainer.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegenbinding.types 2 | 3 | import kotlin.reflect.KClass 4 | 5 | /** 6 | * @param R Type of the runtime type of this fields container. 7 | */ 8 | @Suppress("unused") 9 | interface MetaFieldsContainer { 10 | /** 11 | * Returns the runtime type of this ObjectType as [KClass]. 12 | */ 13 | val runtimeType: KClass 14 | 15 | /** 16 | * Returns all available fields on this ObjectType. 17 | */ 18 | val fields: Set> 19 | } 20 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen-binding/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegenbinding/types/MetaInterfaceType.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegenbinding.types 2 | 3 | /** 4 | * @param R Type of the runtime type of this fields container. 5 | */ 6 | interface MetaInterfaceType : MetaFieldsContainer 7 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen-binding/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegenbinding/types/MetaObjectType.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegenbinding.types 2 | 3 | /** 4 | * @param R Type of the runtime type of this ObjectType. 5 | */ 6 | @Suppress("unused") 7 | interface MetaObjectType : MetaFieldsContainer 8 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen-binding/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegenbinding/types/Value.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegenbinding.types 2 | 3 | /** 4 | * Implementation of a simple data class which holds just one value of type [T]. 5 | * This is required by the double-nullability feature. 6 | * 7 | * @param T Type of the [value] 8 | * @param value Object which is wrapped by this. 9 | */ 10 | @Suppress("unused") 11 | data class Value( 12 | val value: T 13 | ) 14 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/build.gradle.kts: -------------------------------------------------------------------------------- 1 | ext { 2 | this["publication.enabled"] = true 3 | this["publication.artifactId"] = "codegen" 4 | this["publication.name"] = "GraphQL Kotlin Toolkit: Codegen" 5 | this["publication.description"] = "GraphQL Code generator for Kotlin" 6 | this["jacoco.merge.enabled"] = true 7 | } 8 | 9 | dependencies { 10 | implementation("com.graphql-java:graphql-java:16.2") 11 | implementation("com.squareup:kotlinpoet:1.7.2") 12 | 13 | implementation(project(":graphql-kotlin-toolkit-common")) 14 | 15 | testImplementation("com.github.tschuchortdev:kotlin-compile-testing:1.3.1") 16 | testImplementation("org.jetbrains.kotlin:kotlin-reflect") 17 | 18 | testImplementation(project(":graphql-kotlin-toolkit-codegen-binding")) 19 | } 20 | 21 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/Codegen.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen.codeblock.ArgumentCodeBlockGenerator 4 | import com.auritylab.graphql.kotlin.toolkit.codegen.generator.GeneratorFactory 5 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.GeneratedMapper 6 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.ImplementerMapper 7 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.KotlinTypeMapper 8 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.BindingMapper 9 | import com.auritylab.graphql.kotlin.toolkit.common.directive.DirectiveFacade 10 | import java.nio.file.Files 11 | import java.nio.file.Path 12 | 13 | /** 14 | * Represents the base class for the code generation. 15 | */ 16 | class Codegen( 17 | private val options: CodegenOptions 18 | ) { 19 | private val schema = CodegenSchemaParser(options).parseSchemas(options.schemas) 20 | private val supportMapper = BindingMapper() 21 | private val nameMapper = GeneratedMapper(options) 22 | private val kotlinTypeMapper = KotlinTypeMapper(options, nameMapper, supportMapper) 23 | private val implementerMapper = ImplementerMapper(options, schema) 24 | private val outputDirectory = getOutputDirectory() 25 | private val argumentCodeBlockGenerator = ArgumentCodeBlockGenerator(kotlinTypeMapper, supportMapper, nameMapper) 26 | private val generatorFactory = 27 | GeneratorFactory( 28 | options, 29 | kotlinTypeMapper, 30 | nameMapper, 31 | argumentCodeBlockGenerator, 32 | implementerMapper, 33 | supportMapper 34 | ) 35 | 36 | /** 37 | * Will generate code for the types of the [schema]. 38 | */ 39 | fun generate() { 40 | // Validate the directives. 41 | DirectiveFacade.validateAllOnSchema(schema) 42 | 43 | // Build the generators using the CodegenController. 44 | CodegenController(options, schema.allTypesAsList, generatorFactory) 45 | .buildGenerators() 46 | .forEach { 47 | it.generate() 48 | .writeTo(outputDirectory) 49 | } 50 | } 51 | 52 | /** 53 | * Will return the output directory [Path] from the [options]. 54 | * This method will also ensure that the directories exist. 55 | */ 56 | private fun getOutputDirectory(): Path { 57 | val directory = options.outputDirectory 58 | 59 | // Ensure the existence of the base output directory. 60 | Files.createDirectories(directory) 61 | 62 | return directory 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/CodegenOptions.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen 2 | 3 | import java.nio.file.Path 4 | 5 | /** 6 | * Describes configurable options for the code generator. 7 | */ 8 | data class CodegenOptions( 9 | /** 10 | * Describes a [Collection] of [Path]s which point to schemas. 11 | * This property is necessary! 12 | */ 13 | val schemas: Collection, 14 | 15 | /** 16 | * Describes a [Path] which acts as output directory for the generated code. 17 | */ 18 | val outputDirectory: Path, 19 | 20 | /** 21 | * Describes a global prefix for all generated files and classes/enums/etc. 22 | * Defaults to `null` (no prefix). 23 | */ 24 | var generatedGlobalPrefix: String? = null, 25 | 26 | /** 27 | * Describes the name of the packages which contains all generated code. 28 | */ 29 | var generatedBasePackage: String = "graphql.kotlin.toolkit.codegen", 30 | 31 | /** 32 | * Describes if the code shall be generated for all found types, enums, etc. 33 | * If this is [true] it will ignore the generate directive. 34 | */ 35 | var generateAll: Boolean = true, 36 | 37 | /** 38 | * Describes if the generated code should contain additional code 39 | * which can be used to simplify usage with the spring boot integration. 40 | */ 41 | var enableSpringBootIntegration: Boolean = false, 42 | 43 | /** 44 | * Describes the class which will be used as a global context for all resolvers. 45 | */ 46 | var globalContext: String? = null 47 | ) 48 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/CodegenSchemaParser.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen 2 | 3 | import graphql.schema.GraphQLSchema 4 | import graphql.schema.idl.SchemaParser 5 | import graphql.schema.idl.TypeDefinitionRegistry 6 | import graphql.schema.idl.UnExecutableSchemaGenerator 7 | import java.nio.file.Path 8 | 9 | /** 10 | * Represents a parser which takes the input schemas from the [CodegenOptions] and create a [GraphQLSchema] 11 | */ 12 | internal class CodegenSchemaParser( 13 | private val options: CodegenOptions 14 | ) { 15 | private val parser = SchemaParser() 16 | 17 | /** 18 | * Takes the given [files] (which shall be GraphQL schema files) and create a executable [GraphQLSchema]. 19 | */ 20 | fun parseSchemas(files: Collection): GraphQLSchema { 21 | // Create a empty registry. 22 | val baseRegistry = TypeDefinitionRegistry() 23 | 24 | // Go through each given schema file, parse the schema and merge it with the base registry. 25 | files.forEach { 26 | baseRegistry.merge(parser.parse(it.toFile())) 27 | } 28 | 29 | // Create a UnExecutable schema with the created type registry. 30 | return UnExecutableSchemaGenerator.makeUnExecutableSchema(baseRegistry) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/generator/AbstractClassGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.generator 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen.CodegenOptions 4 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.GeneratedMapper 5 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.KotlinTypeMapper 6 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.BindingMapper 7 | import com.squareup.kotlinpoet.ClassName 8 | import com.squareup.kotlinpoet.FileSpec 9 | import com.squareup.kotlinpoet.TypeName 10 | import graphql.schema.GraphQLDirectiveContainer 11 | import graphql.schema.GraphQLNamedType 12 | import graphql.schema.GraphQLType 13 | 14 | internal abstract class AbstractClassGenerator( 15 | protected val options: CodegenOptions, 16 | protected val kotlinTypeMapper: KotlinTypeMapper, 17 | protected val generatedMapper: GeneratedMapper, 18 | protected val bindingMapper: BindingMapper, 19 | ) : FileGenerator { 20 | /** 21 | * Defines the [ClassName] which is used to defined the [FileSpec.packageName] and the [FileSpec.name] 22 | * for the generated [FileSpec]. 23 | */ 24 | protected abstract val fileClassName: ClassName 25 | 26 | /** 27 | * Will configure the [FileSpec.Builder] with the required types, etc. 28 | */ 29 | protected abstract fun build(builder: FileSpec.Builder) 30 | 31 | override fun generate(): FileSpec { 32 | // Create the FileSpec builder using the fileClassName. 33 | val builder = FileSpec.builder(fileClassName.packageName, fileClassName.simpleName) 34 | 35 | // Use the builder method to configure the file. 36 | build(builder) 37 | 38 | // Build the file. 39 | return builder.build() 40 | } 41 | 42 | /** 43 | * Will build the corresponding [TypeName] for the given [GraphQLType]. 44 | * A [fieldDirectiveContainer] can be given additional to determine if a DoubleNull type is required. 45 | */ 46 | protected fun getKotlinType( 47 | type: GraphQLType, 48 | fieldDirectiveContainer: GraphQLDirectiveContainer? = null, 49 | listType: ClassName? = null 50 | ): TypeName = 51 | kotlinTypeMapper.getKotlinType(type, fieldDirectiveContainer, listType) 52 | 53 | /** 54 | * Will build the corresponding [ClassName] for the given [GraphQLType]. 55 | */ 56 | protected fun getGeneratedType(type: GraphQLNamedType): ClassName = 57 | generatedMapper.getGeneratedTypeClassName(type) 58 | } 59 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/generator/AbstractInputDataClassGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.generator 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen.CodegenOptions 4 | import com.auritylab.graphql.kotlin.toolkit.codegen.codeblock.ArgumentCodeBlockGenerator 5 | import com.auritylab.graphql.kotlin.toolkit.codegen.helper.NamingHelper 6 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.GeneratedMapper 7 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.KotlinTypeMapper 8 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.BindingMapper 9 | import com.squareup.kotlinpoet.ANY 10 | import com.squareup.kotlinpoet.FileSpec 11 | import com.squareup.kotlinpoet.FunSpec 12 | import com.squareup.kotlinpoet.KModifier 13 | import com.squareup.kotlinpoet.MAP 14 | import com.squareup.kotlinpoet.MemberName 15 | import com.squareup.kotlinpoet.ParameterSpec 16 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy 17 | import com.squareup.kotlinpoet.PropertySpec 18 | import com.squareup.kotlinpoet.STRING 19 | import com.squareup.kotlinpoet.TypeSpec 20 | import graphql.schema.GraphQLDirectiveContainer 21 | import graphql.schema.GraphQLType 22 | 23 | /** 24 | * Describes an abstract [FileGenerator] which only builds data classes based 25 | * on given properties ([dataProperties]). 26 | */ 27 | internal abstract class AbstractInputDataClassGenerator( 28 | private val argumentCodeBlockGenerator: ArgumentCodeBlockGenerator, 29 | options: CodegenOptions, 30 | kotlinTypeMapper: KotlinTypeMapper, 31 | generatedMapper: GeneratedMapper, bindingMapper: BindingMapper 32 | ) : AbstractClassGenerator(options, kotlinTypeMapper, generatedMapper, bindingMapper) { 33 | override fun build(builder: FileSpec.Builder) { 34 | builder.addType(buildDataClass()) 35 | } 36 | 37 | protected abstract val dataProperties: List 38 | 39 | protected open val buildByMapMemberName: MemberName 40 | get() = MemberName(fileClassName, "buildByMap") 41 | 42 | private fun buildDataClass(): TypeSpec { 43 | val properties = dataProperties 44 | .map { Pair(it.name, getKotlinType(it.type, it.directiveContainer)) } 45 | 46 | return TypeSpec.classBuilder(fileClassName) 47 | .addModifiers(KModifier.DATA) 48 | .primaryConstructor( 49 | FunSpec.constructorBuilder() 50 | .addParameters(properties.map { ParameterSpec(it.first, it.second) }) 51 | .build() 52 | ) 53 | .addProperties(properties.map { PropertySpec.builder(it.first, it.second).initializer(it.first).build() }) 54 | .addType(buildDataClassCompanionObject()) 55 | .build() 56 | } 57 | 58 | private fun buildDataClassCompanionObject(): TypeSpec { 59 | return TypeSpec.companionObjectBuilder() 60 | .addFunction(createBuilderFun()) 61 | .addFunctions( 62 | dataProperties 63 | .map { 64 | argumentCodeBlockGenerator.buildResolver( 65 | it.name, 66 | it.type, 67 | it.directiveContainer 68 | ) 69 | } 70 | ) 71 | .build() 72 | } 73 | 74 | /** 75 | * Will build the "buildByMap" function which takes the input map and builds the data class. 76 | */ 77 | private fun createBuilderFun(): FunSpec { 78 | val namedParameters = dataProperties 79 | .joinToString(", ") { "resolve${NamingHelper.uppercaseFirstLetter(it.name)}(map)" } 80 | return FunSpec.builder(buildByMapMemberName.simpleName) 81 | .addParameter("map", MAP.parameterizedBy(STRING, ANY)) 82 | .returns(fileClassName) 83 | .addStatement("return %T($namedParameters)", fileClassName) 84 | .build() 85 | } 86 | 87 | protected data class DataProperty( 88 | val name: String, 89 | val type: GraphQLType, 90 | val directiveContainer: GraphQLDirectiveContainer? = null 91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/generator/FileGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.generator 2 | 3 | import com.squareup.kotlinpoet.FileSpec 4 | 5 | /** 6 | * Describes a generator which produces a [FileSpec]. 7 | */ 8 | interface FileGenerator { 9 | /** 10 | * Will generate the code and output a [FileSpec]. 11 | */ 12 | fun generate(): FileSpec 13 | } 14 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/generator/GeneratorFactory.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.generator 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen.CodegenOptions 4 | import com.auritylab.graphql.kotlin.toolkit.codegen.codeblock.ArgumentCodeBlockGenerator 5 | import com.auritylab.graphql.kotlin.toolkit.codegen.generator.fieldResolver.FieldResolverGenerator 6 | import com.auritylab.graphql.kotlin.toolkit.codegen.generator.fieldResolver.PaginationFieldResolverGenerator 7 | import com.auritylab.graphql.kotlin.toolkit.codegen.generator.meta.MetaFieldsContainerGenerator 8 | import com.auritylab.graphql.kotlin.toolkit.codegen.generator.pagination.PaginationConnectionGenerator 9 | import com.auritylab.graphql.kotlin.toolkit.codegen.generator.pagination.PaginationEdgeGenerator 10 | import com.auritylab.graphql.kotlin.toolkit.codegen.generator.pagination.PaginationInfoGenerator 11 | import com.auritylab.graphql.kotlin.toolkit.codegen.generator.pagination.PaginationPageInfoGenerator 12 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.GeneratedMapper 13 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.ImplementerMapper 14 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.KotlinTypeMapper 15 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.BindingMapper 16 | import graphql.schema.GraphQLEnumType 17 | import graphql.schema.GraphQLFieldDefinition 18 | import graphql.schema.GraphQLFieldsContainer 19 | import graphql.schema.GraphQLInputObjectType 20 | import graphql.schema.GraphQLObjectType 21 | 22 | internal class GeneratorFactory( 23 | private val options: CodegenOptions, 24 | private val kotlinTypeMapper: KotlinTypeMapper, 25 | private val generatedMapper: GeneratedMapper, 26 | private val argumentCodeBlockGenerator: ArgumentCodeBlockGenerator, 27 | private val implementerMapper: ImplementerMapper, 28 | private val bindingMapper: BindingMapper 29 | ) { 30 | fun enum(enum: GraphQLEnumType): EnumGenerator = 31 | EnumGenerator(enum, options, kotlinTypeMapper, generatedMapper, bindingMapper) 32 | 33 | fun fieldResolver(container: GraphQLFieldsContainer, field: GraphQLFieldDefinition): FieldResolverGenerator = 34 | FieldResolverGenerator( 35 | container, 36 | field, 37 | implementerMapper, 38 | argumentCodeBlockGenerator, 39 | options, 40 | kotlinTypeMapper, 41 | generatedMapper, 42 | bindingMapper, 43 | ) 44 | 45 | fun paginationFieldResolver( 46 | container: GraphQLFieldsContainer, 47 | field: GraphQLFieldDefinition 48 | ): PaginationFieldResolverGenerator = 49 | PaginationFieldResolverGenerator( 50 | container, 51 | field, 52 | implementerMapper, 53 | argumentCodeBlockGenerator, 54 | options, 55 | kotlinTypeMapper, 56 | generatedMapper, 57 | bindingMapper, 58 | ) 59 | 60 | fun inputObject(inputObject: GraphQLInputObjectType): InputObjectGenerator = 61 | InputObjectGenerator( 62 | inputObject, 63 | argumentCodeBlockGenerator, 64 | options, 65 | kotlinTypeMapper, 66 | generatedMapper, 67 | bindingMapper, 68 | ) 69 | 70 | fun objectType(objectType: GraphQLObjectType): ObjectTypeGenerator = 71 | ObjectTypeGenerator(objectType, options, kotlinTypeMapper, generatedMapper, bindingMapper) 72 | 73 | fun fieldsContainerMeta(fieldsContainer: GraphQLFieldsContainer): MetaFieldsContainerGenerator = 74 | MetaFieldsContainerGenerator(fieldsContainer, options, kotlinTypeMapper, generatedMapper, bindingMapper) 75 | 76 | fun paginationInfo(): PaginationInfoGenerator = 77 | PaginationInfoGenerator( 78 | argumentCodeBlockGenerator, 79 | options, 80 | kotlinTypeMapper, 81 | generatedMapper, 82 | bindingMapper, 83 | ) 84 | 85 | fun paginationConnection() = 86 | PaginationConnectionGenerator(options, kotlinTypeMapper, generatedMapper, bindingMapper) 87 | 88 | fun paginationEdge() = PaginationEdgeGenerator(options, kotlinTypeMapper, generatedMapper, bindingMapper) 89 | 90 | fun paginationPageInfo() = 91 | PaginationPageInfoGenerator( 92 | argumentCodeBlockGenerator, 93 | options, 94 | kotlinTypeMapper, 95 | generatedMapper, 96 | bindingMapper, 97 | ) 98 | } 99 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/generator/InputObjectGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.generator 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen.CodegenOptions 4 | import com.auritylab.graphql.kotlin.toolkit.codegen.codeblock.ArgumentCodeBlockGenerator 5 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.GeneratedMapper 6 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.KotlinTypeMapper 7 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.BindingMapper 8 | import com.squareup.kotlinpoet.ClassName 9 | import com.squareup.kotlinpoet.MemberName 10 | import graphql.schema.GraphQLInputObjectType 11 | 12 | /** 13 | * Implements a [AbstractClassGenerator] which will generate the source code for a [GraphQLInputObjectType]. 14 | * It will generate the actual `data class` and a method which can parse a map to the `data class` 15 | */ 16 | internal class InputObjectGenerator( 17 | inputObjectType: GraphQLInputObjectType, 18 | argumentCodeBlockGenerator: ArgumentCodeBlockGenerator, 19 | options: CodegenOptions, 20 | kotlinTypeMapper: KotlinTypeMapper, 21 | generatedMapper: GeneratedMapper, 22 | bindingMapper: BindingMapper 23 | ) : AbstractInputDataClassGenerator( 24 | argumentCodeBlockGenerator, options, kotlinTypeMapper, generatedMapper, bindingMapper 25 | ) { 26 | override val fileClassName: ClassName = getGeneratedType(inputObjectType) 27 | 28 | override val dataProperties: List = inputObjectType.fields 29 | .map { DataProperty(it.name, it.type, it) } 30 | 31 | override val buildByMapMemberName: MemberName = generatedMapper.getInputObjectBuilderMemberName(inputObjectType) 32 | } 33 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/generator/ObjectTypeGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.generator 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen.CodegenOptions 4 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.GeneratedMapper 5 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.KotlinTypeMapper 6 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.BindingMapper 7 | import com.auritylab.graphql.kotlin.toolkit.common.directive.DirectiveFacade 8 | import com.squareup.kotlinpoet.ClassName 9 | import com.squareup.kotlinpoet.FileSpec 10 | import com.squareup.kotlinpoet.FunSpec 11 | import com.squareup.kotlinpoet.ParameterSpec 12 | import com.squareup.kotlinpoet.PropertySpec 13 | import com.squareup.kotlinpoet.TypeSpec 14 | import graphql.schema.GraphQLFieldDefinition 15 | import graphql.schema.GraphQLObjectType 16 | 17 | internal class ObjectTypeGenerator( 18 | private val objectType: GraphQLObjectType, 19 | options: CodegenOptions, 20 | kotlinTypeMapper: KotlinTypeMapper, 21 | generatedMapper: GeneratedMapper, 22 | bindingMapper: BindingMapper, 23 | ) : AbstractClassGenerator(options, kotlinTypeMapper, generatedMapper, bindingMapper) { 24 | override val fileClassName: ClassName = getGeneratedType(objectType) 25 | 26 | override fun build(builder: FileSpec.Builder) { 27 | builder.addType(buildObjectDataClass(objectType)) 28 | } 29 | 30 | private fun buildObjectDataClass(objectType: GraphQLObjectType): TypeSpec { 31 | return TypeSpec.classBuilder(getGeneratedType(objectType)) 32 | .primaryConstructor( 33 | FunSpec.constructorBuilder() 34 | .addParameters(buildParameters(objectType.fieldDefinitions)) 35 | .build() 36 | ) 37 | .addProperties(buildProperties(objectType.fieldDefinitions)) 38 | .build() 39 | } 40 | 41 | private fun buildParameters(fields: Collection): Collection { 42 | return fields 43 | .filter { !DirectiveFacade.Defaults.resolver[it] } 44 | .map { 45 | ParameterSpec.builder(it.name, getKotlinType(it.type)).build() 46 | } 47 | } 48 | 49 | private fun buildProperties(fields: Collection): Collection { 50 | return fields 51 | .filter { !DirectiveFacade.Defaults.resolver[it] } 52 | .map { 53 | PropertySpec.builder(it.name, getKotlinType(it.type)) 54 | .initializer(it.name) 55 | .build() 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/generator/fieldResolver/FieldResolverGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.generator.fieldResolver 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen.CodegenOptions 4 | import com.auritylab.graphql.kotlin.toolkit.codegen.codeblock.ArgumentCodeBlockGenerator 5 | import com.auritylab.graphql.kotlin.toolkit.codegen.helper.NamingHelper 6 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.BindingMapper 7 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.GeneratedMapper 8 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.ImplementerMapper 9 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.KotlinTypeMapper 10 | import com.auritylab.graphql.kotlin.toolkit.common.helper.GraphQLTypeHelper 11 | import com.squareup.kotlinpoet.ClassName 12 | import com.squareup.kotlinpoet.FunSpec 13 | import com.squareup.kotlinpoet.KModifier 14 | import com.squareup.kotlinpoet.NOTHING 15 | import com.squareup.kotlinpoet.ParameterSpec 16 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy 17 | import com.squareup.kotlinpoet.TypeName 18 | import com.squareup.kotlinpoet.TypeSpec 19 | import graphql.schema.GraphQLFieldDefinition 20 | import graphql.schema.GraphQLFieldsContainer 21 | import graphql.schema.GraphQLObjectType 22 | 23 | internal class FieldResolverGenerator( 24 | container: GraphQLFieldsContainer, 25 | field: GraphQLFieldDefinition, 26 | implementerMapper: ImplementerMapper, 27 | argumentCodeBlockGenerator: ArgumentCodeBlockGenerator, 28 | options: CodegenOptions, 29 | kotlinTypeMapper: KotlinTypeMapper, 30 | generatedMapper: GeneratedMapper, 31 | bindingMapper: BindingMapper 32 | ) : AbstractFieldResolverGenerator( 33 | container, 34 | field, 35 | argumentCodeBlockGenerator, 36 | implementerMapper, 37 | options, 38 | kotlinTypeMapper, 39 | generatedMapper, bindingMapper 40 | ) { 41 | override fun buildFieldResolverClass(builder: TypeSpec.Builder) { 42 | builder 43 | .addType(environmentTypeSpec) 44 | 45 | builder.addFunction( 46 | FunSpec.builder("resolve") 47 | .addModifiers(KModifier.ABSTRACT) 48 | .addParameters(field.arguments.map { ParameterSpec(it.name, getKotlinType(it.type, it)) }) 49 | .addParameter("env", generatedMapper.getFieldResolverEnvironment(container, field)) 50 | .returns(fieldTypeName).build() 51 | ) 52 | 53 | builder.addFunction( 54 | FunSpec.builder("get") 55 | .addModifiers(KModifier.OVERRIDE) 56 | .addParameter("env", ClassName("graphql.schema", "DataFetchingEnvironment")) 57 | .returns(getKotlinType(field.type)) 58 | .also { getFunSpec -> 59 | val resolveArgs = field.arguments.let { args -> 60 | if (args.isEmpty()) 61 | return@let "" 62 | 63 | return@let args.joinToString(", ") { 64 | "${it.name} = resolve${NamingHelper.uppercaseFirstLetter(it.name)}(map)" 65 | } + ", " 66 | } 67 | 68 | // Variable assignment is unnecessary if there are no arguments. 69 | if (field.arguments.isNotEmpty()) 70 | getFunSpec.addStatement("val map = env.arguments") 71 | 72 | getFunSpec.addStatement( 73 | "return resolve(${resolveArgs}env = %T(env))", 74 | generatedMapper.getFieldResolverEnvironment(container, field) 75 | ) 76 | } 77 | .build() 78 | ) 79 | } 80 | 81 | private val environmentTypeSpec = 82 | TypeSpec.classBuilder(generatedMapper.getFieldResolverEnvironment(container, field)) 83 | .superclass( 84 | bindingMapper.abstractEnvType.parameterizedBy( 85 | parentTypeName.copy(false), 86 | contextClassName, 87 | resolveEnvMetaType(), 88 | ) 89 | ) 90 | .addSuperclassConstructorParameter("original, %L", resolveTypeMeta()) 91 | .primaryConstructor( 92 | FunSpec.constructorBuilder() 93 | .addParameter("original", dataFetchingEnvironmentClassName) 94 | .build() 95 | ) 96 | .build() 97 | 98 | private fun resolveEnvMetaType(): TypeName { 99 | val fieldType = GraphQLTypeHelper.unwrapType(field.type) 100 | if (fieldType !is GraphQLObjectType) 101 | return NOTHING.copy(true) 102 | 103 | return generatedMapper.getObjectTypeMetaClassName(fieldType) 104 | } 105 | 106 | private fun resolveTypeMeta(): String { 107 | val fieldType = GraphQLTypeHelper.unwrapType(field.type) 108 | if (fieldType !is GraphQLObjectType) 109 | return "null" 110 | 111 | return generatedMapper.getObjectTypeMetaClassName(fieldType).toString() 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/generator/pagination/PaginationConnectionGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.generator.pagination 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen.CodegenOptions 4 | import com.auritylab.graphql.kotlin.toolkit.codegen.generator.AbstractClassGenerator 5 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.GeneratedMapper 6 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.KotlinTypeMapper 7 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.BindingMapper 8 | import com.squareup.kotlinpoet.ANY 9 | import com.squareup.kotlinpoet.ClassName 10 | import com.squareup.kotlinpoet.FileSpec 11 | import com.squareup.kotlinpoet.FunSpec 12 | import com.squareup.kotlinpoet.LIST 13 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy 14 | import com.squareup.kotlinpoet.PropertySpec 15 | import com.squareup.kotlinpoet.TypeSpec 16 | import com.squareup.kotlinpoet.TypeVariableName 17 | 18 | internal class PaginationConnectionGenerator( 19 | options: CodegenOptions, 20 | kotlinTypeMapper: KotlinTypeMapper, 21 | generatedMapper: GeneratedMapper, 22 | bindingMapper: BindingMapper 23 | ) : AbstractClassGenerator(options, kotlinTypeMapper, generatedMapper, bindingMapper) { 24 | override val fileClassName: ClassName = generatedMapper.getPaginationConnectionClassName() 25 | 26 | override fun build(builder: FileSpec.Builder) { 27 | builder.addType(buildConnectionClass()) 28 | } 29 | 30 | private fun buildConnectionClass(): TypeSpec { 31 | val typeVariable = TypeVariableName("T", ANY.copy(true)) 32 | val edgesType = LIST.parameterizedBy(generatedMapper.getPaginationEdgeClassName().parameterizedBy(typeVariable)) 33 | val pageInfoType = generatedMapper.getPaginationPageInfoClassName() 34 | 35 | return TypeSpec.classBuilder(fileClassName) 36 | .addTypeVariable(typeVariable) 37 | .primaryConstructor( 38 | FunSpec.constructorBuilder() 39 | .addParameter("edges", edgesType) 40 | .addParameter("pageInfo", pageInfoType) 41 | .build() 42 | ) 43 | .addProperty(PropertySpec.builder("edges", edgesType).initializer("edges").build()) 44 | .addProperty(PropertySpec.builder("pageInfo", pageInfoType).initializer("pageInfo").build()) 45 | .build() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/generator/pagination/PaginationEdgeGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.generator.pagination 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen.CodegenOptions 4 | import com.auritylab.graphql.kotlin.toolkit.codegen.generator.AbstractClassGenerator 5 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.GeneratedMapper 6 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.KotlinTypeMapper 7 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.BindingMapper 8 | import com.squareup.kotlinpoet.ANY 9 | import com.squareup.kotlinpoet.ClassName 10 | import com.squareup.kotlinpoet.FileSpec 11 | import com.squareup.kotlinpoet.FunSpec 12 | import com.squareup.kotlinpoet.PropertySpec 13 | import com.squareup.kotlinpoet.STRING 14 | import com.squareup.kotlinpoet.TypeSpec 15 | import com.squareup.kotlinpoet.TypeVariableName 16 | 17 | internal class PaginationEdgeGenerator( 18 | options: CodegenOptions, 19 | kotlinTypeMapper: KotlinTypeMapper, 20 | generatedMapper: GeneratedMapper, 21 | bindingMapper: BindingMapper 22 | ) : AbstractClassGenerator(options, kotlinTypeMapper, generatedMapper, bindingMapper) { 23 | override val fileClassName: ClassName = generatedMapper.getPaginationEdgeClassName() 24 | 25 | override fun build(builder: FileSpec.Builder) { 26 | builder.addType(buildEdgeClass()) 27 | } 28 | 29 | private fun buildEdgeClass(): TypeSpec { 30 | val typeVariable = TypeVariableName("T", ANY.copy(true)) 31 | val cursorType = STRING 32 | 33 | return TypeSpec.classBuilder(fileClassName) 34 | .addTypeVariable(typeVariable) 35 | .primaryConstructor( 36 | FunSpec.constructorBuilder() 37 | .addParameter("node", typeVariable) 38 | .addParameter("cursor", cursorType) 39 | .build() 40 | ) 41 | .addProperty(PropertySpec.builder("node", typeVariable).initializer("node").build()) 42 | .addProperty(PropertySpec.builder("cursor", cursorType).initializer("cursor").build()) 43 | .build() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/generator/pagination/PaginationInfoGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.generator.pagination 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen.CodegenOptions 4 | import com.auritylab.graphql.kotlin.toolkit.codegen.codeblock.ArgumentCodeBlockGenerator 5 | import com.auritylab.graphql.kotlin.toolkit.codegen.generator.AbstractInputDataClassGenerator 6 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.GeneratedMapper 7 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.KotlinTypeMapper 8 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.BindingMapper 9 | import com.squareup.kotlinpoet.ClassName 10 | import graphql.Scalars 11 | 12 | internal class PaginationInfoGenerator( 13 | argumentCodeBlockGenerator: ArgumentCodeBlockGenerator, 14 | options: CodegenOptions, 15 | kotlinTypeMapper: KotlinTypeMapper, 16 | generatedMapper: GeneratedMapper, 17 | bindingMapper: BindingMapper 18 | ) : AbstractInputDataClassGenerator( 19 | argumentCodeBlockGenerator, options, kotlinTypeMapper, generatedMapper, bindingMapper 20 | ) { 21 | override val fileClassName: ClassName = 22 | generatedMapper.getPaginationInfoClassName() 23 | 24 | override val dataProperties: List = 25 | listOf( 26 | DataProperty( 27 | "first", 28 | Scalars.GraphQLInt 29 | ), 30 | DataProperty( 31 | "last", 32 | Scalars.GraphQLInt 33 | ), 34 | DataProperty( 35 | "after", 36 | Scalars.GraphQLString 37 | ), 38 | DataProperty( 39 | "before", 40 | Scalars.GraphQLString 41 | ) 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/generator/pagination/PaginationPageInfoGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.generator.pagination 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen.CodegenOptions 4 | import com.auritylab.graphql.kotlin.toolkit.codegen.codeblock.ArgumentCodeBlockGenerator 5 | import com.auritylab.graphql.kotlin.toolkit.codegen.generator.AbstractInputDataClassGenerator 6 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.GeneratedMapper 7 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.KotlinTypeMapper 8 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.BindingMapper 9 | import com.squareup.kotlinpoet.ClassName 10 | import graphql.Scalars 11 | import graphql.schema.GraphQLNonNull 12 | 13 | internal class PaginationPageInfoGenerator( 14 | argumentCodeBlockGenerator: ArgumentCodeBlockGenerator, 15 | options: CodegenOptions, 16 | kotlinTypeMapper: KotlinTypeMapper, 17 | generatedMapper: GeneratedMapper, 18 | bindingMapper: BindingMapper 19 | ) : AbstractInputDataClassGenerator( 20 | argumentCodeBlockGenerator, options, kotlinTypeMapper, generatedMapper, bindingMapper 21 | ) { 22 | override val fileClassName: ClassName = generatedMapper.getPaginationPageInfoClassName() 23 | 24 | override val dataProperties: List = listOf( 25 | DataProperty("hasNextPage", GraphQLNonNull(Scalars.GraphQLBoolean)), 26 | DataProperty("hasPreviousPage", GraphQLNonNull(Scalars.GraphQLBoolean)), 27 | DataProperty("startCursor", Scalars.GraphQLString), 28 | DataProperty("endCursor", Scalars.GraphQLString) 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/helper/GraphQLNameHelper.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.helper 2 | 3 | import graphql.schema.GraphQLNamedSchemaElement 4 | import graphql.schema.GraphQLNamedType 5 | import graphql.schema.GraphQLSchemaElement 6 | import graphql.schema.GraphQLType 7 | 8 | /** 9 | * Object which implements various helpers for the naming of [GraphQLType]. 10 | */ 11 | object GraphQLNameHelper { 12 | /** 13 | * Will build a readable name for the given [type]. 14 | */ 15 | fun buildReadableName(type: GraphQLType): String { 16 | return when (type) { 17 | is GraphQLNamedType -> type.name 18 | else -> type.toString() 19 | } 20 | } 21 | 22 | /** 23 | * WIll build a readable name for the given [element]. 24 | */ 25 | fun buildReadableName(element: GraphQLSchemaElement): String { 26 | return when (element) { 27 | is GraphQLNamedSchemaElement -> element.name 28 | else -> element.toString() 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * @see GraphQLNameHelper.buildReadableName 35 | */ 36 | fun GraphQLType.toReadableName(): String { 37 | return GraphQLNameHelper.buildReadableName(this) 38 | } 39 | 40 | /** 41 | * @see GraphQLNameHelper.buildReadableName 42 | */ 43 | fun GraphQLSchemaElement.toReadableName(): String { 44 | return GraphQLNameHelper.buildReadableName(this) 45 | } 46 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/helper/GraphQLWrapTypeHelper.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.helper 2 | 3 | import com.squareup.kotlinpoet.COLLECTION 4 | import com.squareup.kotlinpoet.ClassName 5 | import com.squareup.kotlinpoet.LIST 6 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy 7 | import com.squareup.kotlinpoet.TypeName 8 | import graphql.schema.GraphQLList 9 | import graphql.schema.GraphQLModifiedType 10 | import graphql.schema.GraphQLNonNull 11 | import graphql.schema.GraphQLType 12 | 13 | internal object GraphQLWrapTypeHelper { 14 | 15 | /** 16 | * Will wrap the given [kotlinType] with the same wrapping of the given [type]. 17 | * If the wrapped type will be used for a output the parameter [isOutput] shall be set to true. 18 | */ 19 | fun wrapType(type: GraphQLType, kotlinType: TypeName, isOutput: Boolean, listType: ClassName? = null): TypeName = 20 | internalWrapType(type, null, kotlinType, isOutput, listType) 21 | 22 | /** 23 | * Will wrap the given [kotlinType] with the same wrapping of the given [type]. This method can be supplied with 24 | * the [parentType] to define the nullability. An explicit [listType] can be defined through the parameter. As the 25 | * explicit [listType] is optional it will fallback to the decision logic which relies on the [isOutput] parameter. 26 | * 27 | * @param listType The type to use to represent a [GraphQLList]. It is to have exactly one type variable. 28 | */ 29 | private fun internalWrapType( 30 | type: GraphQLType, 31 | parentType: GraphQLType?, 32 | kotlinType: TypeName, 33 | isOutput: Boolean, 34 | listType: ClassName? = null 35 | ): TypeName { 36 | return when (type) { 37 | !is GraphQLModifiedType -> { 38 | // Per default all types are nullable in GraphQL, 39 | // therefore always return nullable types for top level types. 40 | if (parentType == null) 41 | return kotlinType.copy(true) 42 | 43 | // If the unmodified type is reached access the parent and check if it's NoNull. 44 | return if (parentType is GraphQLNonNull) 45 | kotlinType.copy(false) 46 | else 47 | kotlinType.copy(true) 48 | } 49 | is GraphQLList -> { 50 | // Create the wrapped type of the wrapped type of the list. 51 | val inner = internalWrapType(type.wrappedType, type, kotlinType, isOutput) 52 | 53 | // If there is an explicit type for the list given use it. If there is no explicit list type 54 | // given use a collection if the type is used for a output type, a List if not. 55 | val list = listType?.parameterizedBy(inner) 56 | ?: (if (isOutput) COLLECTION else LIST).parameterizedBy(inner) 57 | 58 | // Check if the parent is NonNull. 59 | if (parentType is GraphQLNonNull) 60 | list.copy(false) 61 | else 62 | list.copy(true) 63 | } 64 | is GraphQLNonNull -> { 65 | // If there is a NonNull type just delegate to the wrapped type. 66 | return internalWrapType(type.wrappedType, type, kotlinType, isOutput) 67 | } 68 | else -> kotlinType 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/helper/NamingHelper.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.helper 2 | 3 | /** 4 | * A global helper object which provides some common used methods to work with strings. 5 | */ 6 | internal object NamingHelper { 7 | /** 8 | * Will uppercase the first letter of the given [String]. To be more precise, if [i] is "getUser", this method 9 | * will return "GetUser". 10 | * 11 | * @param i The string which shall be transformed. 12 | * @return The transformed string. 13 | */ 14 | fun uppercaseFirstLetter(i: String): String = (i.substring(0, 1).toUpperCase()) + i.substring(1) 15 | 16 | /** 17 | * Will lowercase the first letter for the given [String]. To be more precise, if [i] is "GetUser", this method 18 | * will return "getUser" 19 | * @param i The string which shall be transformed. 20 | * @return The transformed string. 21 | */ 22 | fun lowercaseFirstLetter(i: String): String = (i.substring(0, 1).toLowerCase()) + i.substring(1) 23 | } 24 | 25 | /** 26 | * Extension function which delegates to [NamingHelper.uppercaseFirstLetter]. 27 | * 28 | * @see NamingHelper.uppercaseFirstLetter 29 | */ 30 | internal fun String.uppercaseFirst(): String = NamingHelper.uppercaseFirstLetter(this) 31 | 32 | /** 33 | * Extension function which delegates to [NamingHelper.lowercaseFirstLetter]. 34 | * 35 | * @see NamingHelper.lowercaseFirstLetter 36 | */ 37 | internal fun String.lowercaseFirst(): String = NamingHelper.lowercaseFirstLetter(this) 38 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/helper/SpringBootIntegrationHelper.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.helper 2 | 3 | import com.squareup.kotlinpoet.AnnotationSpec 4 | import com.squareup.kotlinpoet.ClassName 5 | import com.squareup.kotlinpoet.MemberName 6 | 7 | /** 8 | * Represents a helper which provides additional code the spring boot integration. 9 | */ 10 | internal object SpringBootIntegrationHelper { 11 | private val directiveAnnotation = ClassName( 12 | "com.auritylab.graphql.kotlin.toolkit.spring.annotation", 13 | "GQLDirective" 14 | ) 15 | private val resolverAnnotation = ClassName( 16 | "com.auritylab.graphql.kotlin.toolkit.spring.annotation", 17 | "GQLResolver" 18 | ) 19 | private val resolversAnnotation = ClassName( 20 | "com.auritylab.graphql.kotlin.toolkit.spring.annotation", 21 | "GQLResolvers" 22 | ) 23 | private val scalarAnnotation = ClassName( 24 | "com.auritylab.graphql.kotlin.toolkit.spring.annotation", 25 | "GQLScalar" 26 | ) 27 | private val typeResolverAnnotation = ClassName( 28 | "com.auritylab.graphql.kotlin.toolkit.spring.annotation", 29 | "GQLTypeResolver" 30 | ) 31 | 32 | /** 33 | * Will create a annotation which points to the GQLResolver annotation from the spring boot integration. 34 | * The given [container] and [field] will be added to the annotation. 35 | */ 36 | fun createResolverAnnotation(container: MemberName, field: MemberName): AnnotationSpec { 37 | return AnnotationSpec.builder(resolverAnnotation) 38 | .addMember("%M, %M", container, field) 39 | .build() 40 | } 41 | 42 | /** 43 | * Will create a annotation which points to the GQLResolvers annotation from the spring boot integration. 44 | * The given [resolvers] is a collection of pairs where [Pair.first] is the container and [Pair.second] is the field. 45 | */ 46 | fun createMultiResolverAnnotation(resolvers: Collection>): AnnotationSpec { 47 | return AnnotationSpec.builder(resolversAnnotation) 48 | .also { 49 | resolvers.forEach { resolver -> 50 | it.addMember("%T(\"%L\", \"%L\")", resolverAnnotation, resolver.first, resolver.second) 51 | } 52 | } 53 | .build() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/mapper/BindingMapper.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.mapper 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | 5 | /** 6 | * Mapping for the types provided by the support module. 7 | */ 8 | class BindingMapper { 9 | val valueType: ClassName = ClassName("com.auritylab.graphql.kotlin.toolkit.codegenbinding.types", "Value") 10 | 11 | val abstractEnvType: ClassName = 12 | ClassName("com.auritylab.graphql.kotlin.toolkit.codegenbinding.types", "AbstractEnv") 13 | 14 | val metaField = ClassName("com.auritylab.graphql.kotlin.toolkit.codegenbinding.types", "MetaField") 15 | val metaFieldWithReference = 16 | ClassName("com.auritylab.graphql.kotlin.toolkit.codegenbinding.types", "MetaFieldWithReference") 17 | 18 | val metaObjectType = ClassName("com.auritylab.graphql.kotlin.toolkit.codegenbinding.types", "MetaObjectType") 19 | val metaInterfaceType = ClassName("com.auritylab.graphql.kotlin.toolkit.codegenbinding.types", "MetaInterfaceType") 20 | } 21 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/mapper/ImplementerMapper.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.mapper 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen.CodegenOptions 4 | import graphql.schema.GraphQLInterfaceType 5 | import graphql.schema.GraphQLObjectType 6 | import graphql.schema.GraphQLSchema 7 | 8 | /** 9 | * Maps the [GraphQLInterfaceType] to their implementors. 10 | */ 11 | internal class ImplementerMapper( 12 | private val options: CodegenOptions, 13 | private val schema: GraphQLSchema 14 | ) { 15 | /** 16 | * Will search for all implementers for the given [GraphQLInterfaceType] and return them. 17 | */ 18 | fun getImplementers(input: GraphQLInterfaceType): Collection { 19 | return schema.allTypesAsList 20 | .filterIsInstance() 21 | .filter { it != input && schema.isPossibleType(input, it) } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/CodegenSchemaParserTest.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen._test.TestObject 4 | import org.junit.jupiter.api.Assertions.assertNotNull 5 | import org.junit.jupiter.api.Test 6 | import java.nio.file.Path 7 | 8 | internal class CodegenSchemaParserTest { 9 | @Test 10 | fun `should parse single schema properly`() { 11 | val schema = CodegenSchemaParser(TestObject.options).parseSchemas(setOf(testSchemaPath)) 12 | 13 | // Assert that the User type is available... 14 | assertNotNull(schema.getObjectType("User")) 15 | } 16 | 17 | private val testSchemaPath = Path.of( 18 | Thread.currentThread().contextClassLoader.getResource("testschema.graphqls")!!.toURI() 19 | ) 20 | } 21 | 22 | 23 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/_test/AbstractCompilationTest.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen._test 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen.generator.FileGenerator 4 | import com.squareup.kotlinpoet.ClassName 5 | import com.squareup.kotlinpoet.FileSpec 6 | import com.squareup.kotlinpoet.KModifier 7 | import com.squareup.kotlinpoet.TypeSpec 8 | import com.tschuchort.compiletesting.KotlinCompilation 9 | import com.tschuchort.compiletesting.SourceFile 10 | import kotlin.reflect.KClass 11 | 12 | abstract class AbstractCompilationTest( 13 | private val createInterfaceMocks: Boolean = false 14 | ) { 15 | protected open fun compile(generator: FileGenerator) = internalCompile(generator.generate()) 16 | protected open fun compile(fileSpec: FileSpec) = internalCompile(fileSpec) 17 | protected open fun compile(main: FileSpec, vararg dependencies: FileSpec) = internalCompile(main, *dependencies) 18 | protected open fun compile(main: FileGenerator, vararg dependencies: FileGenerator) = 19 | internalCompile(main.generate(), *dependencies.map { it.generate() }.toTypedArray()) 20 | 21 | /** 22 | * Will compile the given [main] and the given [dependencies]. This function will return the runtime reflection 23 | * reference to the [main] class. The [dependencies] will just be added to the compilation. 24 | */ 25 | private fun internalCompile(main: FileSpec, vararg dependencies: FileSpec): Result { 26 | // Generate source files for the given file specs. 27 | val mainSource = buildSourceFile(main, 0) 28 | val dependencySources = dependencies.mapIndexed() { index, dep -> buildSourceFile(dep, index + 1) } 29 | 30 | // Configure the compilation and run the compiler. 31 | val result = KotlinCompilation().apply { 32 | // / Add the generated sources to the compilation. 33 | sources = listOf(mainSource, *dependencySources.toTypedArray()) 34 | inheritClassPath = true 35 | }.compile() 36 | 37 | // Throw an exception if the compilation exited with an unsuccessful exit code. 38 | if (result.exitCode != KotlinCompilation.ExitCode.OK) 39 | throw IllegalStateException("Kotlin compilation not successful!") 40 | 41 | // Load the given main class using the ClassLoader. 42 | return Result(result.classLoader.loadClass(main.packageName + "." + main.name).kotlin, result.classLoader) 43 | } 44 | 45 | /** 46 | * Will build the [SourceFile] based on the given [spec]. 47 | * This will use the name of the class and append the ".kt" extension. 48 | */ 49 | private fun buildSourceFile(spec: FileSpec, counter: Int): SourceFile { 50 | val file: FileSpec = if (createInterfaceMocks) { 51 | // Search on the members of the given file spec for types of kind interface. 52 | val possibleInterfaces = spec.members 53 | .filterIsInstance() 54 | .filter { it.kind == TypeSpec.Kind.INTERFACE } 55 | 56 | val builder = spec.toBuilder() 57 | 58 | possibleInterfaces 59 | .forEach { type -> builder.addType(buildMockImplementation(spec, type)) } 60 | 61 | builder.build() 62 | } else 63 | spec 64 | 65 | return SourceFile.kotlin(spec.name + counter + ".kt", file.toString()) 66 | } 67 | 68 | private fun buildMockImplementation(file: FileSpec, spec: TypeSpec): TypeSpec { 69 | val classToMock = ClassName(file.packageName, spec.name!!) 70 | 71 | return TypeSpec.classBuilder(spec.name!! + "Mock") 72 | .addSuperinterface(classToMock) 73 | .addFunctions( 74 | spec.funSpecs 75 | .filter { KModifier.ABSTRACT in it.modifiers } 76 | .map { 77 | it.toBuilder() 78 | .addModifiers(KModifier.OVERRIDE) 79 | .addCode("TODO()") 80 | .also { t -> t.modifiers.remove(KModifier.ABSTRACT) } 81 | .build() 82 | } 83 | ).build() 84 | } 85 | 86 | data class Result( 87 | val main: KClass<*>, 88 | val classLoader: ClassLoader 89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/_test/TestObject.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen._test 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen.CodegenOptions 4 | import com.auritylab.graphql.kotlin.toolkit.codegen.codeblock.ArgumentCodeBlockGenerator 5 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.BindingMapper 6 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.GeneratedMapper 7 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.ImplementerMapper 8 | import com.auritylab.graphql.kotlin.toolkit.codegen.mapper.KotlinTypeMapper 9 | import graphql.Scalars 10 | import graphql.schema.GraphQLObjectType 11 | import graphql.schema.GraphQLSchema 12 | import java.io.File 13 | 14 | internal object TestObject { 15 | val options = CodegenOptions(hashSetOf(), File("").toPath(), generateAll = false) 16 | val generatedMapper = GeneratedMapper(options) 17 | val supportMapper = BindingMapper() 18 | val kotlinTypeMapper = KotlinTypeMapper(options, generatedMapper, supportMapper) 19 | val argumentCodeBlockGenerator = ArgumentCodeBlockGenerator(kotlinTypeMapper, supportMapper, generatedMapper) 20 | val implementerMapper = ImplementerMapper(options, schema) 21 | 22 | val schema: GraphQLSchema 23 | get() = GraphQLSchema.newSchema() 24 | .query(GraphQLObjectType.newObject().name("Query") 25 | .field { 26 | it.name("testOnQuery") 27 | it.type(Scalars.GraphQLString) 28 | } 29 | .build()) 30 | .mutation(GraphQLObjectType.newObject().name("Mutation") 31 | .field { 32 | it.name("testOnQuery") 33 | it.type(Scalars.GraphQLString) 34 | } 35 | .build()) 36 | .build() 37 | } 38 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/_test/TestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen._test 2 | 3 | import graphql.Scalars 4 | import graphql.schema.GraphQLDirective 5 | import graphql.schema.GraphQLList 6 | import graphql.schema.GraphQLNonNull 7 | import graphql.schema.GraphQLObjectType 8 | 9 | object TestUtils { 10 | /** 11 | * Will build a 'kRepresentation' directive with the given [clazz] as value for the class argument. 12 | */ 13 | fun getRepresentationDirective( 14 | clazz: String = "kotlin.String", 15 | parameters: List? = null 16 | ): GraphQLDirective = 17 | GraphQLDirective.newDirective().name("kRepresentation") 18 | .argument { arg -> 19 | arg.name("class") 20 | arg.type(Scalars.GraphQLString) 21 | arg.value(clazz) 22 | } 23 | .argument { arg -> 24 | arg.name("parameters") 25 | arg.type(GraphQLList(GraphQLNonNull(Scalars.GraphQLString))) 26 | arg.value(parameters) 27 | } 28 | .build() 29 | 30 | /** 31 | * Will create a new dummy ObjectType with the givne [name] and the given [directives]. 32 | */ 33 | fun getDummyObjectType( 34 | name: String = "TestObjectType", 35 | vararg directives: GraphQLDirective 36 | ): GraphQLObjectType = 37 | GraphQLObjectType.newObject() 38 | .name(name) 39 | .withDirectives(*directives) 40 | .build() 41 | } 42 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/codeblock/ArgumentCodeBlockGeneratorTest.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.codeblock 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen._test.AbstractCompilationTest 4 | import com.auritylab.graphql.kotlin.toolkit.codegen._test.TestObject 5 | import com.squareup.kotlinpoet.FileSpec 6 | import com.squareup.kotlinpoet.FunSpec 7 | import com.squareup.kotlinpoet.TypeSpec 8 | import graphql.Scalars 9 | import org.junit.jupiter.api.Assertions 10 | import org.junit.jupiter.api.Test 11 | import kotlin.reflect.full.declaredFunctions 12 | import kotlin.reflect.full.functions 13 | import kotlin.reflect.jvm.isAccessible 14 | 15 | internal class ArgumentCodeBlockGeneratorTest : AbstractCompilationTest() { 16 | 17 | @Test 18 | fun `should build code block for simple scalar correctly`() { 19 | val generator = ArgumentCodeBlockGenerator( 20 | TestObject.kotlinTypeMapper, 21 | TestObject.supportMapper, 22 | TestObject.generatedMapper, 23 | ) 24 | 25 | val resolver = generator.buildResolver("argument", Scalars.GraphQLString, null) 26 | 27 | // Wrap the resolver and compile the code. 28 | val compiled = compile(getWrapperClass(resolver)).main 29 | 30 | // There has to be exactly one function. 31 | Assertions.assertEquals(1, compiled.declaredFunctions.size) 32 | 33 | // Assert against the resolver function. 34 | val function = compiled.functions.first() 35 | // To access the function using reflection. 36 | function.isAccessible = true 37 | 38 | Assertions.assertEquals("resolveArgument", function.name) 39 | 40 | // Call the generated function. 41 | val firstCallResult = function.call(compiled.objectInstance, mapOf("argument" to "test")) 42 | Assertions.assertNotNull(firstCallResult) 43 | Assertions.assertEquals(String::class, firstCallResult!!::class) 44 | Assertions.assertEquals("test", firstCallResult) 45 | } 46 | 47 | /** 48 | * Will create a wrapped class for the given [funSpec]. This will return the usable [FileSpec]. 49 | * 50 | * @param funSpec The function to wrap into a class. 51 | * @return The FileSpec which contains a class which then contains the given [funSpec]. 52 | */ 53 | private fun getWrapperClass(funSpec: FunSpec): FileSpec { 54 | return FileSpec.get("com.auritylab.test", TypeSpec.objectBuilder("Generated").addFunction(funSpec).build()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/generator/InputObjectGeneratorTest.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.generator 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen._test.AbstractCompilationTest 4 | import com.auritylab.graphql.kotlin.toolkit.codegen._test.TestObject 5 | import graphql.Scalars 6 | import graphql.schema.GraphQLInputObjectField 7 | import graphql.schema.GraphQLInputObjectType 8 | import graphql.schema.GraphQLNonNull 9 | import org.junit.jupiter.api.Assertions 10 | import org.junit.jupiter.api.BeforeAll 11 | import org.junit.jupiter.api.Test 12 | import org.junit.jupiter.api.TestInstance 13 | import kotlin.reflect.KClass 14 | import kotlin.reflect.full.companionObject 15 | import kotlin.reflect.full.memberFunctions 16 | import kotlin.reflect.full.memberProperties 17 | import kotlin.reflect.full.starProjectedType 18 | 19 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 20 | internal class InputObjectGeneratorTest : AbstractCompilationTest() { 21 | val generator = InputObjectGenerator( 22 | testInputObject, 23 | TestObject.argumentCodeBlockGenerator, 24 | TestObject.options, 25 | TestObject.kotlinTypeMapper, 26 | TestObject.generatedMapper, 27 | TestObject.supportMapper, 28 | ) 29 | 30 | lateinit var generatedClass: KClass<*> 31 | 32 | @BeforeAll 33 | fun compileCode() { 34 | // Compile the code of the generator 35 | generatedClass = compile(generator).main 36 | } 37 | 38 | @Test 39 | fun `should generate properties correctly`() { 40 | // Assert against the member properties. 41 | val memberProperties = generatedClass.memberProperties.toList() 42 | Assertions.assertEquals(2, memberProperties.size) 43 | Assertions.assertEquals("name", memberProperties[0].name) 44 | Assertions.assertEquals("number", memberProperties[1].name) 45 | } 46 | 47 | @Test 48 | fun `should generate build method correctly`() { 49 | Assertions.assertNotNull(generatedClass.companionObject) 50 | val companionObject = generatedClass.companionObject!! 51 | 52 | // Assert against the member functions. 53 | val memberFunctions = companionObject.memberFunctions 54 | 55 | // Search for the builder method 56 | val builderMethod = memberFunctions.firstOrNull { it.name.endsWith("BuildByMap") } 57 | Assertions.assertNotNull(builderMethod) 58 | 59 | // Cast to not null. 60 | builderMethod!! 61 | 62 | // Assert that the return type of the function is the generated type. 63 | Assertions.assertEquals(generatedClass.starProjectedType, builderMethod.returnType) 64 | 65 | val builderMethodParameters = builderMethod.parameters 66 | val mapBuilderMethodParameter = builderMethodParameters.firstOrNull { it.name == "map" } 67 | Assertions.assertNotNull(mapBuilderMethodParameter) 68 | } 69 | } 70 | 71 | val testInputObject: GraphQLInputObjectType = GraphQLInputObjectType.newInputObject() 72 | .name("TestInput") 73 | .field( 74 | GraphQLInputObjectField.newInputObjectField() 75 | .name("name") 76 | .type(GraphQLNonNull(Scalars.GraphQLString)) 77 | .build() 78 | ) 79 | .field( 80 | GraphQLInputObjectField.newInputObjectField() 81 | .name("number") 82 | .type(Scalars.GraphQLInt) 83 | .build() 84 | ) 85 | .build() 86 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/codegen/generator/ObjectTypeGeneratorTest.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.codegen.generator 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen._test.AbstractCompilationTest 4 | import com.auritylab.graphql.kotlin.toolkit.codegen._test.TestObject 5 | import graphql.Scalars 6 | import graphql.schema.GraphQLDirective 7 | import graphql.schema.GraphQLFieldDefinition 8 | import graphql.schema.GraphQLObjectType 9 | import org.junit.jupiter.api.Assertions 10 | import org.junit.jupiter.api.BeforeAll 11 | import org.junit.jupiter.api.Test 12 | import org.junit.jupiter.api.TestInstance 13 | import kotlin.reflect.KClass 14 | import kotlin.reflect.full.memberProperties 15 | 16 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 17 | internal class ObjectTypeGeneratorTest : AbstractCompilationTest() { 18 | val generator = ObjectTypeGenerator( 19 | testObjectType, 20 | TestObject.options, 21 | TestObject.kotlinTypeMapper, 22 | TestObject.generatedMapper, 23 | TestObject.supportMapper, 24 | ) 25 | 26 | lateinit var generatedClass: KClass<*> 27 | 28 | @BeforeAll 29 | fun compileCode() { 30 | // Compile the code of the generator 31 | generatedClass = compile(generator).main 32 | } 33 | 34 | @Test 35 | fun `should generate properties correctly`() { 36 | val memberProperties = generatedClass.memberProperties.toList() 37 | Assertions.assertEquals(2, memberProperties.size) 38 | 39 | Assertions.assertEquals("name", memberProperties[0].name) 40 | Assertions.assertEquals("number", memberProperties[1].name) 41 | 42 | // "hasResolver" MUST NOT be present as it's annotated with kResolver. 43 | } 44 | } 45 | 46 | val testObjectType: GraphQLObjectType = GraphQLObjectType.newObject() 47 | .name("TestObjectType") 48 | .field( 49 | GraphQLFieldDefinition.newFieldDefinition() 50 | .name("name") 51 | .type(Scalars.GraphQLString) 52 | ).field( 53 | GraphQLFieldDefinition.newFieldDefinition() 54 | .name("number") 55 | .type(Scalars.GraphQLInt) 56 | ) 57 | .field( 58 | GraphQLFieldDefinition.newFieldDefinition() 59 | .name("hasResolver") 60 | .type(Scalars.GraphQLInt) 61 | .withDirective(GraphQLDirective.newDirective().name("kResolver")) 62 | ) 63 | .build() 64 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-codegen/src/test/resources/testschema.graphqls: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | mutation: Mutation 4 | } 5 | 6 | type Query { 7 | getUser: User! 8 | getUsers: [User]! 9 | } 10 | 11 | type Mutation { 12 | updateUser(input: UserUpdateInput!): User! 13 | updateUsers(input: [UserUpdateInput]): [User!]! 14 | } 15 | 16 | input UserUpdateInput { 17 | name: String 18 | surname: String 19 | } 20 | 21 | type User { 22 | id: ID! 23 | name: String 24 | surname: String 25 | age: Int 26 | } 27 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | ext { 2 | this["publication.enabled"] = true 3 | this["publication.artifactId"] = "common" 4 | this["publication.name"] = "GraphQL Kotlin Toolkit: Common" 5 | this["publication.description"] = "GraphQL Code generator for Kotlin" 6 | this["jacoco.merge.enabled"] = true 7 | } 8 | 9 | dependencies { 10 | implementation("com.graphql-java:graphql-java:16.2") 11 | } 12 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/common/directive/AbstractDirective.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.common.directive 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.common.directive.exception.DirectiveValidationException 4 | import com.auritylab.graphql.kotlin.toolkit.common.helper.GraphQLEqualityHelper 5 | import graphql.introspection.Introspection 6 | import graphql.schema.GraphQLDirective 7 | import graphql.schema.GraphQLNamedType 8 | import graphql.schema.GraphQLType 9 | 10 | abstract class AbstractDirective( 11 | override val name: String, 12 | ) : Directive { 13 | /** 14 | * Will check if the given [directive] will match against the reference ([reference]). 15 | */ 16 | override fun validateDefinition(directive: GraphQLDirective) { 17 | // Validate the arguments of the directive. 18 | reference.arguments.forEach { 19 | // Resolve the argument by the name and throw exception if not found. 20 | val argOfDirective = directive.getArgument(it.name) 21 | ?: throw buildArgumentNotFoundException(it.name) 22 | 23 | // Check if the type of the reference argument is the same as on the given directive. 24 | if (!GraphQLEqualityHelper.isEqual(argOfDirective.type, it.type)) 25 | throw buildInvalidArgumentTypeException(it.name, it.type) 26 | } 27 | 28 | // Validate the locations of the directive. 29 | val locationsOfDirective = directive.validLocations() 30 | reference.validLocations().forEach { 31 | // Check if the location from the reference exists on the directive. 32 | if (!locationsOfDirective.contains(it)) 33 | throw buildInvalidLocationException(it) 34 | } 35 | } 36 | 37 | /** 38 | * Will build a [DirectiveValidationException] which tells that the argument with the given [name] could not be 39 | * found on the directive definition. 40 | */ 41 | private fun buildArgumentNotFoundException( 42 | name: String 43 | ): DirectiveValidationException = 44 | DirectiveValidationException(this, "Argument '$name' not found on directive") 45 | 46 | /** 47 | * Will build a [DirectiveValidationException] which tells that the argument with the given [argumentName] was 48 | * defined with the wrong type. The [expectedType] tells the expected type for the argument. 49 | */ 50 | private fun buildInvalidArgumentTypeException( 51 | argumentName: String, 52 | expectedType: GraphQLType 53 | ): DirectiveValidationException = 54 | DirectiveValidationException( 55 | this, 56 | "Argument '$argumentName' is expected to be type of '${if (expectedType is GraphQLNamedType) expectedType.name else "unknown"}'" 57 | ) 58 | 59 | /** 60 | * Will build a [DirectiveValidationException] which tells directive requires the given [requiredLocation]. 61 | */ 62 | private fun buildInvalidLocationException( 63 | requiredLocation: Introspection.DirectiveLocation 64 | ): DirectiveValidationException = 65 | DirectiveValidationException(this, "Directive location '${requiredLocation.name}' not found on directive") 66 | } 67 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/common/directive/Directive.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.common.directive 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.common.directive.exception.DirectiveValidationException 4 | import graphql.schema.GraphQLDirective 5 | import graphql.schema.GraphQLDirectiveContainer 6 | 7 | /** 8 | * Describes a directive which is used by the code generator. 9 | */ 10 | interface Directive { 11 | /** 12 | * The name of the directive. 13 | */ 14 | val name: String 15 | 16 | /** 17 | * The reference [GraphQLDirective], which is represented here. 18 | */ 19 | val reference: GraphQLDirective 20 | 21 | /** 22 | * Will validate the definition of given [directive]. This has to validate if all arguments are defined with 23 | * the correct scalar, etc. 24 | * 25 | * @param directive The directive definition to validate against. 26 | * @throws DirectiveValidationException If the validation was not successful. 27 | */ 28 | fun validateDefinition(directive: GraphQLDirective) 29 | 30 | /** 31 | * Will check if this directive exists on the given [container]. 32 | * 33 | * @param container The container to check against. 34 | * @return If this directive exists on the given [container]. 35 | */ 36 | fun existsOnContainer(container: GraphQLDirectiveContainer): Boolean = 37 | container.getDirective(name) != null 38 | 39 | /** 40 | * @see existsOnContainer 41 | */ 42 | operator fun get(container: GraphQLDirectiveContainer): Boolean = 43 | existsOnContainer(container) 44 | } 45 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/common/directive/DirectiveFacade.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.common.directive 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.common.directive.exception.DirectiveValidationException 4 | import com.auritylab.graphql.kotlin.toolkit.common.directive.implementation.DoubleNullDirective 5 | import com.auritylab.graphql.kotlin.toolkit.common.directive.implementation.GenerateDirective 6 | import com.auritylab.graphql.kotlin.toolkit.common.directive.implementation.PaginationDirective 7 | import com.auritylab.graphql.kotlin.toolkit.common.directive.implementation.RepresentationDirective 8 | import com.auritylab.graphql.kotlin.toolkit.common.directive.implementation.ResolverDirective 9 | import graphql.schema.GraphQLDirective 10 | import graphql.schema.GraphQLSchema 11 | 12 | object DirectiveFacade { 13 | // List of all available directives. 14 | private val directivesList = listOf( 15 | Defaults.generate, 16 | Defaults.resolver, 17 | Defaults.representation, 18 | Defaults.doubleNull, 19 | Defaults.pagination 20 | ) 21 | 22 | /** 23 | * Will validate all existing [Directive]s on the given [schema]. 24 | * 25 | * @throws DirectiveValidationException If the validation of any directive fails. 26 | */ 27 | fun validateAllOnSchema(schema: GraphQLSchema) { 28 | directivesList.forEach { codegenDirective -> 29 | getDirectiveDefinition(codegenDirective, schema) 30 | ?.let { codegenDirective.validateDefinition(it) } 31 | } 32 | } 33 | 34 | /** 35 | * Will check if the given [directive] exists on the given [schema] (using [Directive.name]). 36 | * Additional it will return the [GraphQLDirective] if it's available on the schema 37 | * 38 | * @throws DirectiveValidationException If the directive is required but is not present on the [schema]. 39 | */ 40 | private fun getDirectiveDefinition(directive: Directive, schema: GraphQLSchema): GraphQLDirective? { 41 | return schema.getDirective(directive.name) 42 | } 43 | 44 | /** 45 | * Object which contains all available default directives. 46 | */ 47 | object Defaults { 48 | val generate = GenerateDirective 49 | val resolver = ResolverDirective 50 | val representation = RepresentationDirective 51 | val doubleNull = DoubleNullDirective 52 | val pagination = PaginationDirective 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/common/directive/HasArgumentsDirective.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.common.directive 2 | 3 | import graphql.schema.GraphQLDirective 4 | import graphql.schema.GraphQLDirectiveContainer 5 | 6 | /** 7 | * Describes a [Directive], which has arguments. 8 | */ 9 | interface HasArgumentsDirective : Directive { 10 | /** 11 | * Will resolve the given [directive] into an instance of [M], which is a representation of the arguments. 12 | */ 13 | fun getArguments(directive: GraphQLDirective): M 14 | 15 | /** 16 | * Will resolve this directive in the given [container] into an instance of [M], which is a representation of the 17 | * arguments. 18 | */ 19 | fun getArguments(container: GraphQLDirectiveContainer): M? 20 | } 21 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/common/directive/exception/DirectiveValidationException.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.common.directive.exception 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.common.directive.Directive 4 | 5 | /** 6 | * Represents an exception, which will be thrown if the directive validation failed. 7 | * 8 | * @see Directive.validateDefinition 9 | */ 10 | class DirectiveValidationException( 11 | directive: Directive, 12 | message: String 13 | ) : Exception("Directive '${directive.name}': $message") 14 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/common/directive/implementation/DoubleNullDirective.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.common.directive.implementation 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.common.directive.AbstractDirective 4 | import graphql.introspection.Introspection 5 | import graphql.schema.GraphQLDirective 6 | 7 | object DoubleNullDirective : AbstractDirective("kDoubleNull") { 8 | override val reference: GraphQLDirective = 9 | GraphQLDirective.newDirective() 10 | .name(name) 11 | .validLocations( 12 | Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION, 13 | Introspection.DirectiveLocation.ARGUMENT_DEFINITION 14 | ) 15 | .build() 16 | } 17 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/common/directive/implementation/GenerateDirective.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.common.directive.implementation 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.common.directive.AbstractDirective 4 | import graphql.introspection.Introspection 5 | import graphql.schema.GraphQLDirective 6 | 7 | object GenerateDirective : AbstractDirective("kGenerate") { 8 | override val reference: GraphQLDirective = 9 | GraphQLDirective.newDirective() 10 | .name(name) 11 | .validLocations(Introspection.DirectiveLocation.OBJECT) 12 | .build() 13 | } 14 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/common/directive/implementation/PaginationDirective.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.common.directive.implementation 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.common.directive.AbstractDirective 4 | import graphql.introspection.Introspection 5 | import graphql.schema.GraphQLDirective 6 | 7 | object PaginationDirective : AbstractDirective("kPagination") { 8 | override val reference: GraphQLDirective = 9 | GraphQLDirective.newDirective() 10 | .name(name) 11 | .validLocations(Introspection.DirectiveLocation.FIELD_DEFINITION) 12 | .build() 13 | } 14 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/common/directive/implementation/RepresentationDirective.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.common.directive.implementation 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.common.directive.AbstractDirective 4 | import com.auritylab.graphql.kotlin.toolkit.common.directive.HasArgumentsDirective 5 | import graphql.Scalars 6 | import graphql.introspection.Introspection 7 | import graphql.schema.GraphQLDirective 8 | import graphql.schema.GraphQLDirectiveContainer 9 | import graphql.schema.GraphQLList 10 | import graphql.schema.GraphQLNonNull 11 | 12 | object RepresentationDirective : 13 | AbstractDirective("kRepresentation"), 14 | HasArgumentsDirective { 15 | 16 | override val reference: GraphQLDirective = 17 | GraphQLDirective.newDirective() 18 | .name(name) 19 | .validLocations( 20 | Introspection.DirectiveLocation.OBJECT, 21 | Introspection.DirectiveLocation.SCALAR, 22 | Introspection.DirectiveLocation.INTERFACE, 23 | Introspection.DirectiveLocation.ENUM 24 | ) 25 | // Argument which defines the FQN of the representation class. 26 | .argument { 27 | it.name("class") 28 | it.type(GraphQLNonNull(Scalars.GraphQLString)) 29 | } 30 | // Argument which defines a list with parameters for the type. 31 | .argument { 32 | it.name("parameters") 33 | it.type(GraphQLList(GraphQLNonNull(Scalars.GraphQLString))) 34 | } 35 | .build() 36 | 37 | override fun getArguments(directive: GraphQLDirective): Model { 38 | val className = directive.getArgument("class").value as? String ?: "" 39 | val parameters = directive.getArgument("parameters")?.value as? List 40 | 41 | return Model(className, parameters) 42 | } 43 | 44 | override fun getArguments(container: GraphQLDirectiveContainer): Model? { 45 | val directive = container.getDirective(name) 46 | ?: return null 47 | 48 | return getArguments(directive) 49 | } 50 | 51 | data class Model( 52 | /** 53 | * The FQN name of the representing class. 54 | * E.g. "java.util.UUID" 55 | */ 56 | val className: String?, 57 | 58 | /** 59 | * Optional parameters for the representing class. Each entry is a FQN name of a class. 60 | * One exception is a star-projection which is represented as "*". 61 | */ 62 | val parameters: List? 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/common/directive/implementation/ResolverDirective.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.common.directive.implementation 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.common.directive.AbstractDirective 4 | import graphql.introspection.Introspection 5 | import graphql.schema.GraphQLDirective 6 | 7 | object ResolverDirective : AbstractDirective("kResolver") { 8 | override val reference: GraphQLDirective = 9 | GraphQLDirective.newDirective() 10 | .name(name) 11 | .validLocations( 12 | Introspection.DirectiveLocation.FIELD_DEFINITION, 13 | Introspection.DirectiveLocation.OBJECT, 14 | Introspection.DirectiveLocation.INTERFACE 15 | ) 16 | .build() 17 | } 18 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/common/helper/GraphQLEqualityHelper.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.common.helper 2 | 3 | import graphql.com.google.common.base.Objects 4 | import graphql.schema.GraphQLList 5 | import graphql.schema.GraphQLModifiedType 6 | import graphql.schema.GraphQLNonNull 7 | import graphql.schema.GraphQLType 8 | 9 | object GraphQLEqualityHelper { 10 | /** 11 | * Checks if the given [GraphQLType]s are equal. The [GraphQLModifiedType]s are also considered. 12 | */ 13 | fun isEqual(first: GraphQLType, second: GraphQLType): Boolean { 14 | val firstUnwrapped = GraphQLTypeHelper.unwrapTypeLayers(first) 15 | val secondUnwrapped = GraphQLTypeHelper.unwrapTypeLayers(second) 16 | 17 | // If the sizes of the lists do not match, then return false. 18 | if (firstUnwrapped.size != secondUnwrapped.size) { 19 | return false 20 | } 21 | 22 | firstUnwrapped.forEachIndexed { index, type -> 23 | if (type !is GraphQLModifiedType && !Objects.equal(type, secondUnwrapped[index])) { 24 | // If it's not a modified type and the types in both lists do not match, then return false 25 | return false 26 | } 27 | 28 | // If it's a non null type and the types do not match, then return false. 29 | if (type is GraphQLNonNull && !type.isEqualTo(secondUnwrapped[index])) { 30 | return false 31 | } 32 | 33 | // If it's a list type and the types do not match, then return false 34 | if (type is GraphQLList && !type.isEqualTo(secondUnwrapped[index])) { 35 | return false 36 | } 37 | } 38 | 39 | return true 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/common/helper/GraphQLTypeHelper.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.common.helper 2 | 3 | import graphql.schema.GraphQLList 4 | import graphql.schema.GraphQLModifiedType 5 | import graphql.schema.GraphQLNonNull 6 | import graphql.schema.GraphQLType 7 | 8 | object GraphQLTypeHelper { 9 | /** 10 | * Will unwrap the given [type] until the type is no longer instance of [GraphQLModifiedType]. 11 | * This will just return the non [GraphQLModifiedType] aka the most inner type. 12 | * 13 | * @param type The type to unwrap. 14 | * @return The most inner type of the given type. 15 | */ 16 | fun unwrapType(type: GraphQLType): GraphQLType = 17 | unwrapTypeLayers(type).last() 18 | 19 | /** 20 | * Will unwrap the given [type] until the type is no longer instance of [GraphQLModifiedType]. Each layer of the 21 | * unwrapping process will be returned in the [List]. The list is sorted accordingly to the unwrapping. The final 22 | * non [GraphQLModifiedType] will also be added to the last on the last index. 23 | * 24 | * @param type The type to unwrap. 25 | * @return List of all [GraphQLType]. 26 | */ 27 | fun unwrapTypeLayers(type: GraphQLType): List { 28 | val wraps = mutableListOf() 29 | 30 | // Start with the given type. 31 | var c = type 32 | 33 | // Iterate until there is no longer a modified type. 34 | while (c is GraphQLModifiedType) { 35 | // Add the current type to the layers. 36 | wraps.add(c) 37 | // Set the current type to the wrapped type. 38 | c = c.wrappedType 39 | } 40 | 41 | // Add the non modified type to the list. 42 | wraps.add(c) 43 | 44 | return wraps 45 | } 46 | 47 | /** 48 | * Will check if the given [GraphQLType] is a List. This will also check if the first layer is a [GraphQLNonNull]. 49 | * 50 | * @param type The type to check against. 51 | * @return If the given type is [GraphQLList]. 52 | */ 53 | fun isList(type: GraphQLType): Boolean = 54 | getListType(type) != null 55 | 56 | /** 57 | * Will check if the given [type] is a List. If it is a list, it will return the wrapped type of the list. 58 | * 59 | * @param type The type which is expected to be a list. 60 | * @return The wrapped type of the list or null if [type] is no list. 61 | */ 62 | fun getListType(type: GraphQLType): GraphQLType? { 63 | // If the given type is a list itself. 64 | if (type is GraphQLList) 65 | return type.wrappedType 66 | 67 | // If the list is wrapped with a non-null. 68 | if (type is GraphQLNonNull && type.wrappedType is GraphQLList) 69 | return (type.wrappedType as GraphQLList).wrappedType 70 | 71 | return null 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/common/markers/Experimental.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.common.markers 2 | 3 | /** 4 | * Marker for a public API which is in experimental state. Marked elements might be changed or even removed 5 | * in the future. 6 | */ 7 | @Retention(AnnotationRetention.SOURCE) 8 | annotation class Experimental() 9 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/common/directive/AbstractDirectiveTest.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.common.directive 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.common.directive.exception.DirectiveValidationException 4 | import graphql.Scalars 5 | import graphql.introspection.Introspection 6 | import graphql.schema.GraphQLArgument 7 | import graphql.schema.GraphQLDirective 8 | import org.junit.jupiter.api.Nested 9 | import org.junit.jupiter.api.Test 10 | import org.junit.jupiter.api.assertDoesNotThrow 11 | import org.junit.jupiter.api.assertThrows 12 | 13 | internal class AbstractDirectiveTest { 14 | @Nested 15 | inner class ValidateDefinition { 16 | @Test 17 | fun shouldThrowExceptionOnMissingArgument() { 18 | assertThrows { 19 | // _directiveOne defines exactly one argument which is expected to be not available on _directiveEmpty, 20 | // because it does not define any argument. 21 | toInternal(_directiveOne).validateDefinition(_directiveEmpty) 22 | } 23 | } 24 | 25 | @Test 26 | fun shouldIgnoreTooManyArguments() { 27 | assertDoesNotThrow { 28 | // As a reference we have a directive with no arguments. We validate against a directive which defines 29 | // exactly one argument. Because additional directives do not matter here, we just assert that it does 30 | // not throw. 31 | toInternal(_directiveEmpty).validateDefinition(_directiveOne) 32 | } 33 | } 34 | 35 | @Test 36 | fun shouldThrowExceptionOnMissingLocation() { 37 | assertThrows { 38 | // _directiveTwo defines one valid location which is expected to be not available on _directiveEmpty. 39 | // Therefore, we assert that an exception will be thrown. 40 | toInternal(_directiveTwo).validateDefinition(_directiveEmpty) 41 | } 42 | } 43 | 44 | @Test 45 | fun shouldIgnoreTooManyLocations() { 46 | assertDoesNotThrow { 47 | // As a reference we have a directive with no valid locations. We validate against a directive which 48 | // defines exactly one valid location. Because additional locations do not matter here, we assert that 49 | // it does not throw any exception. 50 | toInternal(_directiveEmpty).validateDefinition(_directiveTwo) 51 | } 52 | } 53 | 54 | @Test 55 | fun shouldThrowExceptionOnDifferingArgumentTypes () { 56 | assertThrows { 57 | // As a reference we have a directive with exactly one argument named 'name' of type 'String'. We try 58 | // to validate against a directive which also defines exactly one argument named 'name' but with a 59 | // differing type 'boolean'. Because those types differ we assert that an exception will be thrown. 60 | toInternal(_directiveOne).validateDefinition(_directiveThree) 61 | } 62 | } 63 | } 64 | 65 | private val _directiveEmpty = GraphQLDirective.newDirective() 66 | .name("DirectiveEmpty").build() 67 | 68 | private val _directiveOne = GraphQLDirective.newDirective() 69 | .name("Directive") 70 | .argument( 71 | GraphQLArgument.newArgument() 72 | .name("name") 73 | .type(Scalars.GraphQLString) 74 | ).build() 75 | 76 | private val _directiveTwo = GraphQLDirective.newDirective() 77 | .name("DirectiveTwo") 78 | .validLocations(Introspection.DirectiveLocation.FIELD) 79 | .build() 80 | 81 | private val _directiveThree = GraphQLDirective.newDirective() 82 | .name("DirectiveThree") 83 | .argument(GraphQLArgument.newArgument().name("name").type(Scalars.GraphQLBoolean)) 84 | .build() 85 | 86 | /** 87 | * Creates a new [AbstractDirective] based on the given input parameters. The [input] will be used 88 | * as [AbstractDirective.reference]. All other parameters are optional and define default values. 89 | */ 90 | private fun toInternal( 91 | input: GraphQLDirective, 92 | name: String = "Test", 93 | required: Boolean = false 94 | ): AbstractDirective { 95 | return object : AbstractDirective(name) { 96 | override val reference: GraphQLDirective = input 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/common/directive/DirectiveFacadeTest.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.common.directive 2 | 3 | import graphql.schema.GraphQLSchema 4 | import graphql.schema.idl.SchemaParser 5 | import graphql.schema.idl.TypeDefinitionRegistry 6 | import graphql.schema.idl.UnExecutableSchemaGenerator 7 | import org.junit.jupiter.api.Test 8 | 9 | internal class DirectiveFacadeTest { 10 | @Test 11 | fun shouldValidateCorrectDirectivesProperly() { 12 | // This throws an exception if the validation fails. 13 | DirectiveFacade.validateAllOnSchema(getSchema("allDirectives.graphqls")) 14 | } 15 | 16 | companion object { 17 | private fun getSchema(name: String): GraphQLSchema { 18 | // Load the raw schema from the current class loader. 19 | val rawSchema = 20 | String(Thread.currentThread().contextClassLoader.getResourceAsStream(name)!!.readAllBytes()) 21 | 22 | // Create an executable schema. 23 | return UnExecutableSchemaGenerator.makeUnExecutableSchema( 24 | TypeDefinitionRegistry().merge( 25 | SchemaParser().parse( 26 | rawSchema 27 | ) 28 | ) 29 | ) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/common/helper/GraphQLEqualityHelperTest.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.common.helper 2 | 3 | import graphql.Scalars 4 | import graphql.schema.GraphQLList 5 | import graphql.schema.GraphQLNonNull 6 | import graphql.schema.GraphQLObjectType 7 | import org.junit.jupiter.api.Assertions.* 8 | import org.junit.jupiter.api.Test 9 | 10 | internal class GraphQLEqualityHelperTest { 11 | @Test 12 | fun `should compare same scalars properly`() { 13 | assertTrue(GraphQLEqualityHelper.isEqual(Scalars.GraphQLString, Scalars.GraphQLString)) 14 | } 15 | 16 | @Test 17 | fun `should compare non null wrapped scalars properly`() { 18 | assertTrue( 19 | GraphQLEqualityHelper.isEqual( 20 | GraphQLNonNull(Scalars.GraphQLString), 21 | GraphQLNonNull(Scalars.GraphQLString) 22 | ) 23 | ) 24 | } 25 | 26 | @Test 27 | fun `should compare list wrapped scalars properly`() { 28 | assertTrue( 29 | GraphQLEqualityHelper.isEqual( 30 | GraphQLList(Scalars.GraphQLString), 31 | GraphQLList(Scalars.GraphQLString), 32 | ) 33 | ) 34 | } 35 | 36 | @Test 37 | fun `should compare list wrapped object type properly`() { 38 | val testObject = GraphQLObjectType.newObject() 39 | .name("Test") 40 | .field { 41 | it.name("name") 42 | it.type(Scalars.GraphQLString) 43 | } 44 | .build() 45 | 46 | assertTrue(GraphQLEqualityHelper.isEqual(GraphQLList(testObject), GraphQLList(testObject))) 47 | } 48 | 49 | @Test 50 | fun `should compare one list wrapped type with non list wrapped type properly`() { 51 | assertFalse(GraphQLEqualityHelper.isEqual(GraphQLList(Scalars.GraphQLString), Scalars.GraphQLString)) 52 | } 53 | 54 | @Test 55 | fun `should compare simple unequal types properly`() { 56 | assertFalse(GraphQLEqualityHelper.isEqual(Scalars.GraphQLString, Scalars.GraphQLInt)) 57 | } 58 | 59 | @Test 60 | fun `should compare non matching modifier types properly`() { 61 | assertFalse( 62 | GraphQLEqualityHelper.isEqual( 63 | GraphQLNonNull(Scalars.GraphQLString), 64 | GraphQLList(Scalars.GraphQLString) 65 | ) 66 | ) 67 | } 68 | 69 | @Test 70 | fun `should compare multiple non matching modifier types properly`() { 71 | assertFalse( 72 | GraphQLEqualityHelper.isEqual( 73 | GraphQLList(GraphQLNonNull(Scalars.GraphQLString)), 74 | GraphQLNonNull(GraphQLList(Scalars.GraphQLString)) 75 | ) 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-common/src/test/resources/allDirectives.graphqls: -------------------------------------------------------------------------------- 1 | directive @kRepresentation(class: String!, parameters: [String!]) on OBJECT | SCALAR | INTERFACE | ENUM 2 | directive @kGenerate on OBJECT 3 | directive @kResolver on FIELD_DEFINITION | OBJECT | INTERFACE 4 | directive @kDoubleNull on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION 5 | directive @kPagination on FIELD_DEFINITION 6 | 7 | schema { 8 | query: Query 9 | } 10 | type Query { 11 | test: String 12 | } 13 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-gradle-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-gradle-plugin") 3 | id("com.gradle.plugin-publish") version "0.11.0" 4 | id("org.gradle.kotlin.kotlin-dsl") version "2.1.4" 5 | } 6 | 7 | dependencies { 8 | implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.10") 9 | implementation(gradleApi()) 10 | implementation(project(":graphql-kotlin-toolkit-codegen")) 11 | } 12 | 13 | gradlePlugin { 14 | plugins { 15 | create("graphql-kotlin-toolkit-codegen") { 16 | id = "com.auritylab.graphql-kotlin-toolkit.codegen" 17 | displayName = "GraphQL Kotlin Toolkit: Codegen" 18 | description = "GraphQL code generator for Kotlin" 19 | implementationClass = "com.auritylab.graphql.kotlin.toolkit.gradle.CodegenGradlePlugin" 20 | } 21 | } 22 | } 23 | 24 | pluginBundle { 25 | website = "https://github.com/AurityLab/graphql-kotlin-toolkit" 26 | vcsUrl = "https://github.com/AurityLab/graphql-kotlin-toolkit" 27 | tags = listOf("graphql", "kotlin", "codegen", "codegeneration", "graphql-codegen") 28 | } 29 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-gradle-plugin/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/gradle/CodegenGradlePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.gradle 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.gradle.extension.CodegenExtension 4 | import com.auritylab.graphql.kotlin.toolkit.gradle.task.CodegenTask 5 | import org.gradle.api.Plugin 6 | import org.gradle.api.Project 7 | import org.gradle.kotlin.dsl.create 8 | import org.gradle.kotlin.dsl.findByType 9 | import org.gradle.kotlin.dsl.invoke 10 | import org.gradle.kotlin.dsl.withType 11 | import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension 12 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 13 | 14 | class CodegenGradlePlugin : Plugin { 15 | companion object { 16 | const val PLUGIN_GROUP = "GraphQL Kotlin Codegen" 17 | } 18 | 19 | override fun apply(project: Project) { 20 | val generateExtension = project.extensions.create("graphqlKotlinCodegen") 21 | val defaultOutputDirectory = project.layout.buildDirectory.dir("generated/graphql/kotlin/main/") 22 | 23 | generateExtension.outputDirectory.set(defaultOutputDirectory) 24 | 25 | project.afterEvaluate { 26 | val kotlinProjectExtension = this.extensions.findByType() 27 | ?: throw IllegalStateException("Plugin 'org.jetbrains.kotlin.jvm' not applied.") 28 | 29 | kotlinProjectExtension.sourceSets { 30 | "main" { 31 | kotlin.srcDir(defaultOutputDirectory) 32 | } 33 | } 34 | 35 | tasks.create("graphqlKotlinCodegen", CodegenTask::class) { 36 | group = PLUGIN_GROUP 37 | 38 | schemas.setFrom(generateExtension.schemas) 39 | outputDirectory.set(generateExtension.outputDirectory) 40 | generatedGlobalPrefix.set(generateExtension.generatedGlobalPrefix) 41 | generatedBasePackage.set(generateExtension.generatedBasePackage) 42 | generateAll.set(generateExtension.generateAll) 43 | enableSpringBootIntegration.set(generateExtension.enableSpringBootIntegration) 44 | globalContext.set(generateExtension.globalContext) 45 | } 46 | 47 | tasks.withType() { 48 | this.dependsOn("graphqlKotlinCodegen") 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-gradle-plugin/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/gradle/extension/CodegenExtension.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.gradle.extension 2 | 3 | import org.gradle.api.model.ObjectFactory 4 | import org.gradle.kotlin.dsl.property 5 | 6 | open class CodegenExtension(objects: ObjectFactory) { 7 | val schemas = objects.fileCollection() 8 | 9 | var outputDirectory = objects.directoryProperty() 10 | 11 | val generatedGlobalPrefix = objects.property() 12 | 13 | val generatedBasePackage = objects.property() 14 | 15 | val generateAll = objects.property() 16 | 17 | val enableSpringBootIntegration = objects.property() 18 | 19 | val globalContext = objects.property() 20 | } 21 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-gradle-plugin/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/gradle/task/CodegenTask.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.gradle.task 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegen.Codegen 4 | import com.auritylab.graphql.kotlin.toolkit.codegen.CodegenOptions 5 | import org.gradle.api.DefaultTask 6 | import org.gradle.api.tasks.Input 7 | import org.gradle.api.tasks.InputFiles 8 | import org.gradle.api.tasks.Optional 9 | import org.gradle.api.tasks.OutputDirectory 10 | import org.gradle.api.tasks.TaskAction 11 | import org.gradle.kotlin.dsl.property 12 | import java.nio.file.Path 13 | 14 | open class CodegenTask : DefaultTask() { 15 | @InputFiles 16 | val schemas = project.objects.fileCollection() 17 | 18 | @OutputDirectory 19 | val outputDirectory = project.objects.directoryProperty() 20 | 21 | @Input 22 | @Optional 23 | val generatedGlobalPrefix = project.objects.property() 24 | 25 | @Input 26 | @Optional 27 | val generatedBasePackage = project.objects.property() 28 | 29 | @Input 30 | @Optional 31 | val generateAll = project.objects.property() 32 | 33 | @Input 34 | @Optional 35 | val enableSpringBootIntegration = project.objects.property() 36 | 37 | @Input 38 | @Optional 39 | val globalContext = project.objects.property() 40 | 41 | @TaskAction 42 | fun doGenerate() { 43 | val codegen = Codegen(buildCodegenOptions()) 44 | 45 | // Start the generation process. 46 | codegen.generate() 47 | } 48 | 49 | /** 50 | * Will build a new [CodegenOptions] instance with the given tasks options. 51 | */ 52 | private fun buildCodegenOptions(): CodegenOptions { 53 | val options = CodegenOptions( 54 | getSchemaPaths(), 55 | getOutputDirectoryPath() 56 | ) 57 | 58 | generatedGlobalPrefix.orNull?.also { 59 | options.generatedGlobalPrefix = it 60 | } 61 | 62 | generatedBasePackage.orNull?.also { 63 | options.generatedBasePackage = it 64 | } 65 | 66 | generateAll.orNull?.also { 67 | options.generateAll = it 68 | } 69 | 70 | enableSpringBootIntegration.orNull?.also { 71 | options.enableSpringBootIntegration = it 72 | } 73 | 74 | globalContext.orNull?.also { 75 | options.globalContext = it 76 | } 77 | 78 | return options 79 | } 80 | 81 | /** 82 | * Will return all schema paths in a list. 83 | * If no schemas are set it will throw an exception. 84 | */ 85 | private fun getSchemaPaths(): List { 86 | // Check if there are any schemas. 87 | if (schemas.isEmpty) throw IllegalStateException("No schemas provided") 88 | 89 | // Map the files to paths. 90 | return schemas.files.map { it.toPath() } 91 | } 92 | 93 | /** 94 | * Will return the output directory path. 95 | * If no output directory is set it will throw an exception. 96 | */ 97 | private fun getOutputDirectoryPath(): Path { 98 | if (!outputDirectory.isPresent) 99 | throw IllegalStateException("No output directory provided") 100 | 101 | return outputDirectory.asFile.get().toPath() 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.kapt") 3 | id("org.jetbrains.kotlin.plugin.spring") 4 | } 5 | 6 | ext { 7 | this["publication.enabled"] = true 8 | this["publication.artifactId"] = "spring-boot" 9 | this["publication.name"] = "GraphQL Kotlin Toolkit: Spring" 10 | this["publication.description"] = "GraphQL integration for Spring" 11 | this["jacoco.merge.enabled"] = true 12 | } 13 | 14 | dependencies { 15 | implementation(project(":graphql-kotlin-toolkit-common")) 16 | 17 | // Spring (Boot) dependencies. 18 | implementation("org.springframework.boot:spring-boot-autoconfigure:2.2.2.RELEASE") 19 | implementation("org.springframework:spring-webmvc:5.2.2.RELEASE") 20 | implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.1") 21 | kapt("org.springframework.boot:spring-boot-configuration-processor:2.2.2.RELEASE") 22 | 23 | // GraphQL-Java dependency. 24 | implementation("com.graphql-java:graphql-java:16.2") 25 | 26 | implementation("com.auritylab:kotlin-object-path:1.0.0") 27 | 28 | // Test dependencies. 29 | testImplementation("org.springframework.boot:spring-boot-starter-web:2.2.2.RELEASE") 30 | testImplementation("org.springframework.boot:spring-boot-starter-test:2.2.2.RELEASE") { 31 | exclude(group = "org.junit.vintage", module = "junit-vintage-engine") 32 | } 33 | 34 | testImplementation("com.github.VerachadW:kraph:v.0.6.1") 35 | testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0") 36 | } 37 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/AutoConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring 2 | 3 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication 4 | import org.springframework.context.annotation.ComponentScan 5 | import org.springframework.context.annotation.Configuration 6 | 7 | @Configuration 8 | @ComponentScan 9 | @ConditionalOnWebApplication 10 | class AutoConfiguration 11 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/annotation/GQLDirective.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.annotation 2 | 3 | import graphql.schema.idl.SchemaDirectiveWiring 4 | import org.springframework.stereotype.Component 5 | 6 | /** 7 | * Describes a annotation which shall only be used on [SchemaDirectiveWiring]. 8 | */ 9 | @Component 10 | @Target(AnnotationTarget.CLASS) 11 | @Retention(AnnotationRetention.RUNTIME) 12 | annotation class GQLDirective( 13 | /** 14 | * The name of the directive. 15 | */ 16 | val directive: String 17 | ) 18 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/annotation/GQLResolver.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.annotation 2 | 3 | import graphql.schema.DataFetcher 4 | import org.springframework.stereotype.Component 5 | 6 | /** 7 | * Describes a annotation which shall only be used on [DataFetcher]. 8 | * 9 | */ 10 | @Component 11 | @Target(AnnotationTarget.CLASS) 12 | @Retention(AnnotationRetention.RUNTIME) 13 | annotation class GQLResolver( 14 | /** 15 | * The container in which the field is located. 16 | */ 17 | val container: String, 18 | 19 | /** 20 | * The actual name of the field for which this resolver shall be registered.s 21 | */ 22 | val field: String 23 | ) 24 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/annotation/GQLResolvers.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.annotation 2 | 3 | import org.springframework.stereotype.Component 4 | 5 | /** 6 | * Describes a annotation which contains multiple [GQLResolver]. 7 | * 8 | * @see GQLResolver For further documentation. 9 | */ 10 | @Component 11 | @Target(AnnotationTarget.CLASS) 12 | @Retention(AnnotationRetention.RUNTIME) 13 | annotation class GQLResolvers( 14 | vararg val resolvers: GQLResolver 15 | ) 16 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/annotation/GQLScalar.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.annotation 2 | 3 | import graphql.schema.Coercing 4 | import org.springframework.stereotype.Component 5 | 6 | /** 7 | * Describes a annotation which shall only be used on [Coercing]. 8 | */ 9 | @Component 10 | @Target(AnnotationTarget.CLASS) 11 | @Retention(AnnotationRetention.RUNTIME) 12 | annotation class GQLScalar( 13 | /** 14 | * The actual name of the scalar. 15 | */ 16 | val name: String 17 | ) 18 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/annotation/GQLTypeResolver.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.annotation 2 | 3 | import graphql.schema.TypeResolver 4 | import org.springframework.stereotype.Component 5 | 6 | /** 7 | * Describes a annotation which shall only be used on [TypeResolver]. 8 | * This can either describe a type resolver for a Interface or a Union. 9 | */ 10 | @Component 11 | @Target(AnnotationTarget.CLASS) 12 | @Retention(AnnotationRetention.RUNTIME) 13 | annotation class GQLTypeResolver( 14 | /** 15 | * The actual name of the type. 16 | */ 17 | val type: String, 18 | /** 19 | * If the type is a Interface or a Union. 20 | */ 21 | val scope: Scope 22 | ) { 23 | /** 24 | * Describes the scope for the type resolver. 25 | */ 26 | enum class Scope { 27 | INTERFACE, 28 | UNION 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/api/GraphQLInvocation.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.api 2 | 3 | import graphql.ExecutionResult 4 | import org.springframework.web.context.request.WebRequest 5 | import java.util.concurrent.CompletableFuture 6 | 7 | interface GraphQLInvocation { 8 | /** 9 | * Will execute the GraphQL Query (described in [data]) and return a [CompletableFuture] 10 | * which contains the [ExecutionResult]. The corresponding [WebRequest] is also supplied 11 | * to access additional information 12 | */ 13 | fun invoke(data: Data, request: WebRequest): CompletableFuture 14 | 15 | /** 16 | * Describes the GraphQL Query. 17 | */ 18 | data class Data( 19 | val query: String?, 20 | val operationName: String?, 21 | val variables: Map? 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/api/GraphQLSchemaSupplier.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.api 2 | 3 | /** 4 | * Describes a supplier for multiple GraphQL Schema sources. 5 | */ 6 | interface GraphQLSchemaSupplier { 7 | /** 8 | * The GraphQL schemas as sources. 9 | */ 10 | val schemas: Collection 11 | } 12 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/api/GraphQLSchemaSupplierExtensions.kt: -------------------------------------------------------------------------------- 1 | // Ignore for entire file, because this is a public API. 2 | @file:Suppress("unused") 3 | 4 | package com.auritylab.graphql.kotlin.toolkit.spring.api 5 | 6 | import org.springframework.core.io.DefaultResourceLoader 7 | 8 | /** 9 | * Private implementation of a [GraphQLSchemaSupplier]. 10 | */ 11 | private data class Data(override val schemas: Collection) : GraphQLSchemaSupplier 12 | 13 | /** 14 | * Will use the given [strings] as schemas. 15 | */ 16 | fun schemaOfStrings(strings: Collection): GraphQLSchemaSupplier = 17 | Data(strings) 18 | 19 | /** 20 | * Will use the given [strings] as schema. 21 | */ 22 | fun schemaOfStrings(vararg strings: String): GraphQLSchemaSupplier = 23 | schemaOfStrings(strings.asList()) 24 | 25 | /** 26 | * Will resolve the given [files] and use their content as schemas. 27 | */ 28 | fun schemaOfResourceFiles(files: Collection): GraphQLSchemaSupplier = 29 | Data(files.map { resolveResourceFile(it) }) 30 | 31 | /** 32 | * Will resolve the given [files] and use their content as schemas. 33 | */ 34 | fun schemaOfResourceFiles(vararg files: String): GraphQLSchemaSupplier = 35 | schemaOfResourceFiles(files.asList()) 36 | 37 | /** 38 | * Will search for the given [file] on the classpath. 39 | * If the [file] was found the content will be returned. 40 | */ 41 | private fun resolveResourceFile(file: String): String { 42 | val resource = DefaultResourceLoader().getResource(file) 43 | 44 | // Check if the resource file exists. 45 | if (!resource.exists()) 46 | throw IllegalArgumentException("Schema file '$file' could not be found") 47 | 48 | // Read the content of the file and return it. 49 | return resource.inputStream.reader(Charsets.UTF_8).readText() 50 | } 51 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/configuration/GraphQLConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.configuration 2 | 3 | import graphql.GraphQL 4 | import graphql.execution.AsyncExecutionStrategy 5 | import graphql.execution.DataFetcherExceptionHandler 6 | import graphql.execution.ExecutionStrategy 7 | import graphql.execution.instrumentation.ChainedInstrumentation 8 | import graphql.execution.instrumentation.Instrumentation 9 | import graphql.schema.GraphQLSchema 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean 11 | import org.springframework.context.ApplicationContext 12 | import org.springframework.context.annotation.Bean 13 | import org.springframework.context.annotation.Configuration 14 | import org.springframework.context.annotation.Import 15 | import java.util.Optional 16 | 17 | @Configuration 18 | @Import(SchemaConfiguration::class) 19 | @ConditionalOnMissingBean(GraphQL::class) 20 | class GraphQLConfiguration( 21 | private val context: ApplicationContext, 22 | private val schema: GraphQLSchema, 23 | private val exceptionHandler: Optional 24 | ) { 25 | @Bean 26 | fun configureGraphQL(): GraphQL { 27 | val builder = GraphQL.newGraphQL(schema) 28 | 29 | // Build the Instrumentation and register it if found. 30 | buildInstrumentation() 31 | ?.also { builder.instrumentation(it) } 32 | 33 | // Build the execution strategy and apply it to the builder. 34 | buildExecutionStrategy().also { 35 | builder.queryExecutionStrategy(it) 36 | builder.mutationExecutionStrategy(it) 37 | } 38 | 39 | return builder.build() 40 | } 41 | 42 | /** 43 | * Will fetch all available instances of [Instrumentation] and join them if needed. 44 | * This will utilize the [context] to fetch all beans with type [Instrumentation]. 45 | */ 46 | private fun buildInstrumentation(): Instrumentation? { 47 | // Fetch the beans using the application context. 48 | val instrumentationBeans = context.getBeansOfType(Instrumentation::class.java) 49 | 50 | val allInstrumentation = mutableListOf() 51 | 52 | // Go through each found instrumentation bean and add it to the list. 53 | instrumentationBeans.values.forEach { 54 | // If it's a ChainedInstrumentation we need to add each of them. 55 | if (it is ChainedInstrumentation) 56 | allInstrumentation.addAll(it.instrumentations) 57 | else 58 | allInstrumentation.add(it) 59 | } 60 | 61 | return when { 62 | allInstrumentation.isEmpty() -> null 63 | allInstrumentation.size == 1 -> allInstrumentation[0] 64 | else -> ChainedInstrumentation(allInstrumentation) 65 | } 66 | } 67 | 68 | /** 69 | * Will build a [ExecutionStrategy]. By default this will always build a [AsyncExecutionStrategy], 70 | * optionally with the given [exceptionHandler]. 71 | */ 72 | private fun buildExecutionStrategy(): ExecutionStrategy = 73 | if (exceptionHandler.isPresent) 74 | AsyncExecutionStrategy(exceptionHandler.get()) 75 | else 76 | AsyncExecutionStrategy() 77 | } 78 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/configuration/GraphQLProperties.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.configuration 2 | 3 | import org.springframework.beans.factory.annotation.Value 4 | import org.springframework.boot.context.properties.ConfigurationProperties 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.validation.annotation.Validated 7 | 8 | @Validated 9 | @Configuration 10 | @ConfigurationProperties("graphql-kotlin-toolkit.spring") 11 | open class GraphQLProperties { 12 | /** 13 | * Represents the endpoint for the GraphQL controller. 14 | * Defaults to "graphql". 15 | */ 16 | @Value("graphql") 17 | lateinit var endpoint: String 18 | 19 | /** 20 | * Represents the property to access the instrumentation properties. 21 | */ 22 | var instrumentation: Instrumentation = 23 | Instrumentation() 24 | 25 | /** 26 | * Represents all available options for instrumentations. 27 | */ 28 | open class Instrumentation { 29 | /** 30 | * If the tracing extension should be enabled. 31 | * Defaults to "false". 32 | */ 33 | var enableTracingInstrumentation: Boolean = false 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/configuration/InstrumentationConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.configuration 2 | 3 | import graphql.execution.instrumentation.Instrumentation 4 | import graphql.execution.instrumentation.tracing.TracingInstrumentation 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.context.annotation.Configuration 8 | 9 | /** 10 | * Takes care about registering [Instrumentation] beans based on the properties ([Properties]). 11 | */ 12 | @Configuration 13 | class InstrumentationConfiguration { 14 | /** 15 | * Will conditionally create a bean of type [TracingInstrumentation] if the 16 | * "graphql-kotlin-toolkit.spring.enableTracing" property is set to "true". 17 | */ 18 | @Bean 19 | @ConditionalOnProperty( 20 | prefix = "graphql-kotlin-toolkit.spring.instrumentation", 21 | name = ["enable-tracing-instrumentation"], 22 | havingValue = "true" 23 | ) 24 | fun tracingInstrumentation(): Instrumentation = TracingInstrumentation() 25 | } 26 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/configuration/SchemaConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.configuration 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.annotation.AnnotationResolver 4 | import com.auritylab.graphql.kotlin.toolkit.spring.api.GraphQLSchemaSupplier 5 | import com.auritylab.graphql.kotlin.toolkit.spring.schema.BaseSchemaAugmentation 6 | import graphql.schema.GraphQLSchema 7 | import graphql.schema.idl.RuntimeWiring 8 | import graphql.schema.idl.SchemaGenerator 9 | import graphql.schema.idl.SchemaParser 10 | import graphql.schema.idl.TypeDefinitionRegistry 11 | import graphql.schema.idl.WiringFactory 12 | import org.springframework.beans.factory.getBeansOfType 13 | import org.springframework.context.ApplicationContext 14 | import org.springframework.context.annotation.Bean 15 | import org.springframework.context.annotation.Configuration 16 | 17 | @Configuration 18 | class SchemaConfiguration( 19 | private val context: ApplicationContext, 20 | private val annotationResolver: AnnotationResolver, 21 | private val wiringFactory: WiringFactory 22 | ) { 23 | private val augmentation = BaseSchemaAugmentation() 24 | 25 | @Bean 26 | fun configureSchema(): GraphQLSchema = buildSchema() 27 | 28 | /** 29 | * Will build the [GraphQLSchema]. 30 | */ 31 | private fun buildSchema(): GraphQLSchema { 32 | val suppliers = context.getBeansOfType() 33 | 34 | return when { 35 | suppliers.isNotEmpty() -> parseSchema(fetchSchemaSuppliers(suppliers.values), wiringFactory) 36 | else -> throw IllegalStateException("No GQLSchemaSupplier instance was found.") 37 | } 38 | } 39 | 40 | /** 41 | * Will parse the given [schemas] and create [GraphQLSchema]. 42 | */ 43 | private fun parseSchema(schemas: Collection, wiringFactory: WiringFactory): GraphQLSchema { 44 | val parser = SchemaParser() 45 | val generator = SchemaGenerator() 46 | 47 | // Parse the schema and join them into one registry. 48 | val registry = schemas.map { parser.parse(it) }.reduce(TypeDefinitionRegistry::merge) 49 | 50 | // Create a executable schema. 51 | val generatedSchema = generator.makeExecutableSchema(registry, buildRuntimeWiring(wiringFactory)) 52 | 53 | return augmentation.augmentSchema(generatedSchema) 54 | } 55 | 56 | /** 57 | * Will build a [RuntimeWiring] with the current [wiringFactory] and all directives from the[annotationResolver]. 58 | */ 59 | private fun buildRuntimeWiring(wiringFactory: WiringFactory): RuntimeWiring { 60 | val wiring = RuntimeWiring.newRuntimeWiring() 61 | 62 | // Register the wiring factory. 63 | wiring.wiringFactory(wiringFactory) 64 | 65 | // Register each directive. 66 | annotationResolver.directives.forEach { wiring.directive(it.key.directive, it.value) } 67 | 68 | return wiring.build() 69 | } 70 | 71 | /** 72 | * Will fetch all beans of type [GraphQLSchemaSupplier] and merge all schemas into a single [Collection]. 73 | */ 74 | private fun fetchSchemaSuppliers( 75 | suppliers: Collection 76 | ): Collection = 77 | suppliers.fold( 78 | mutableSetOf(), 79 | { acc, supplier -> 80 | acc.addAll(supplier.schemas) 81 | acc 82 | } 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/controller/AbstractController.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.controller 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.api.GraphQLInvocation 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import com.fasterxml.jackson.module.kotlin.readValue 6 | import org.springframework.web.context.request.WebRequest 7 | import java.util.concurrent.CompletableFuture 8 | 9 | abstract class AbstractController( 10 | protected val objectMapper: ObjectMapper, 11 | private val invocation: GraphQLInvocation 12 | ) { 13 | 14 | /** 15 | * Will execute the given [operation] using the [invocation]. 16 | */ 17 | protected fun execute( 18 | operation: Operation, 19 | request: WebRequest 20 | ): CompletableFuture = 21 | invocation.invoke( 22 | GraphQLInvocation.Data(operation.query, operation.operationName, operation.variables), 23 | request 24 | ).thenApply { it.toSpecification() } 25 | 26 | /** 27 | * Represents a GraphQL operation with a [query], [operationName] (optional) and [variables] (optional). 28 | */ 29 | protected data class Operation( 30 | val query: String = "", 31 | val operationName: String?, 32 | val variables: Map? 33 | ) 34 | 35 | /** 36 | * Will parse the given [input] into [T]. If the given [input] can not be parsed into [T] `null` will be returned. 37 | */ 38 | protected inline fun parse(input: String): T? = 39 | try { 40 | objectMapper.readValue(input) 41 | } catch (ex: Exception) { 42 | null 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/controller/GetController.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.controller 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.api.GraphQLInvocation 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import org.springframework.http.MediaType 6 | import org.springframework.web.bind.annotation.RequestMapping 7 | import org.springframework.web.bind.annotation.RequestMethod 8 | import org.springframework.web.bind.annotation.RequestParam 9 | import org.springframework.web.bind.annotation.RestController 10 | import org.springframework.web.context.request.WebRequest 11 | import java.util.concurrent.CompletableFuture 12 | 13 | @RestController 14 | class GetController( 15 | objectMapper: ObjectMapper, 16 | invocation: GraphQLInvocation 17 | ) : AbstractController(objectMapper, invocation) { 18 | /** 19 | * Will accept GET requests. The [query] has to be preset, [operationName] and [variables] are optional. 20 | * 21 | * See: 22 | * - https://graphql.org/learn/serving-over-http/#get-request 23 | * - https://github.com/APIs-guru/graphql-over-http#get 24 | */ 25 | @RequestMapping( 26 | value = ["\${graphql-kotlin-toolkit.spring.endpoint:graphql}"], 27 | method = [RequestMethod.GET], 28 | produces = [MediaType.APPLICATION_JSON_VALUE] 29 | ) 30 | fun get( 31 | @RequestParam(value = "query") query: String, 32 | @RequestParam(value = "operationName", required = false) operationName: String?, 33 | @RequestParam(value = "variables", required = false) variables: String?, 34 | request: WebRequest 35 | ): CompletableFuture = 36 | execute( 37 | Operation( 38 | query, 39 | operationName, 40 | variables?.let { parse>(it) } 41 | ), 42 | request 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/controller/PostController.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.controller 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.api.GraphQLInvocation 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import org.springframework.http.HttpHeaders 6 | import org.springframework.http.HttpStatus 7 | import org.springframework.http.MediaType 8 | import org.springframework.web.bind.annotation.RequestBody 9 | import org.springframework.web.bind.annotation.RequestHeader 10 | import org.springframework.web.bind.annotation.RequestMapping 11 | import org.springframework.web.bind.annotation.RequestMethod 12 | import org.springframework.web.bind.annotation.RequestParam 13 | import org.springframework.web.bind.annotation.RestController 14 | import org.springframework.web.context.request.WebRequest 15 | import org.springframework.web.server.ResponseStatusException 16 | import java.util.concurrent.CompletableFuture 17 | 18 | @RestController 19 | class PostController( 20 | objectMapper: ObjectMapper, 21 | invocation: GraphQLInvocation 22 | ) : AbstractController(objectMapper, invocation) { 23 | companion object { 24 | private const val GRAPHQL_CONTENT_TYPE_VALUE = "application/graphql" 25 | private val GRAPHQL_CONTENT_TYPE = MediaType.parseMediaType(GRAPHQL_CONTENT_TYPE_VALUE) 26 | } 27 | 28 | /** 29 | * Will accept POST requests. 30 | * 31 | * See: 32 | * - https://graphql.org/learn/serving-over-http/#post-request 33 | * - https://github.com/APIs-guru/graphql-over-http#post 34 | */ 35 | @RequestMapping( 36 | value = ["\${graphql-kotlin-toolkit.spring.endpoint:graphql}"], 37 | method = [RequestMethod.POST], 38 | produces = [MediaType.APPLICATION_JSON_VALUE] 39 | ) 40 | fun post( 41 | @RequestHeader(value = HttpHeaders.CONTENT_TYPE) contentType: String, 42 | @RequestParam(value = "query", required = false) query: String?, 43 | @RequestBody(required = false) body: String?, 44 | request: WebRequest 45 | ): CompletableFuture { 46 | // Parse the given contentType into a MediaType. 47 | val parsedMediaType = MediaType.parseMediaType(contentType) 48 | 49 | // If the body is given and the contentType is application/json just parse the body and execute the data. 50 | if (body != null && parsedMediaType.equalsTypeAndSubtype(MediaType.APPLICATION_JSON)) { 51 | val operation = parse(body) 52 | ?: throw ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "Unable to parse operation") 53 | return execute(operation, request) 54 | } 55 | 56 | // If a body is given and the contentType is application/graphql just use the body as query. 57 | if (body != null && parsedMediaType.equalsTypeAndSubtype(GRAPHQL_CONTENT_TYPE)) 58 | return execute(Operation(body, null, null), request) 59 | 60 | // If the query parameter is give just is it as query. 61 | if (query != null) 62 | return execute(Operation(query, null, null), request) 63 | 64 | // None of the conditions above matched, therefore an error will be thrown.ø 65 | throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Unable to process GraphQL request") 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/controller/UploadController.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.controller 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.api.GraphQLInvocation 4 | import com.auritylab.kotlin.object_path.KObjectPath 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import org.springframework.http.HttpStatus 7 | import org.springframework.http.MediaType 8 | import org.springframework.web.bind.annotation.RequestMapping 9 | import org.springframework.web.bind.annotation.RequestMethod 10 | import org.springframework.web.bind.annotation.RequestParam 11 | import org.springframework.web.bind.annotation.RestController 12 | import org.springframework.web.context.request.WebRequest 13 | import org.springframework.web.multipart.MultipartRequest 14 | import org.springframework.web.server.ResponseStatusException 15 | import java.util.concurrent.CompletableFuture 16 | 17 | @RestController 18 | class UploadController( 19 | objectMapper: ObjectMapper, 20 | invocation: GraphQLInvocation 21 | ) : AbstractController(objectMapper, invocation) { 22 | /** 23 | * Will accept POST (multipart/form-data) requests. 24 | * This implements the graphql-multipart-request-spec. 25 | * 26 | * See: 27 | * - https://github.com/jaydenseric/graphql-multipart-request-spec 28 | */ 29 | @RequestMapping( 30 | value = ["\${graphql-kotlin-toolkit.spring.endpoint:graphql}"], 31 | method = [RequestMethod.POST], 32 | produces = [MediaType.APPLICATION_JSON_VALUE], 33 | consumes = [MediaType.MULTIPART_FORM_DATA_VALUE] 34 | ) 35 | fun postMultipart( 36 | @RequestParam(value = "operations") operations: String, 37 | @RequestParam(value = "map") map: String, 38 | multipartRequest: MultipartRequest, 39 | request: WebRequest 40 | ): CompletableFuture { 41 | val parsedOperation = parse(operations) 42 | ?: throw ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "Unable to parse operation") 43 | val parsedMap = parse>>(map) 44 | ?: throw ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "Unable to parse map") 45 | 46 | parsedMap.forEach { (mapKey, mapValue) -> 47 | // Check if the file exists. 48 | if (!multipartRequest.fileMap.containsKey(mapKey)) 49 | throw ResponseStatusException(HttpStatus.BAD_REQUEST, "File '$mapKey' could not be found") 50 | 51 | mapValue.forEach { path -> 52 | try { 53 | KObjectPath(parsedOperation).path(path).set(multipartRequest.fileMap[mapKey]) 54 | } catch (ex: Exception) { 55 | throw ResponseStatusException( 56 | HttpStatus.BAD_REQUEST, 57 | "Path '$path' of key '$mapKey' could not be found" 58 | ) 59 | } 60 | } 61 | } 62 | 63 | return execute(parsedOperation, request) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/internal/InternalGQLInvocation.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.internal 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.api.GraphQLInvocation 4 | import graphql.ExecutionInput 5 | import graphql.ExecutionResult 6 | import graphql.GraphQL 7 | import org.springframework.stereotype.Component 8 | import org.springframework.web.context.request.WebRequest 9 | import java.util.concurrent.CompletableFuture 10 | 11 | @Component 12 | internal class InternalGQLInvocation( 13 | private val gql: GraphQL 14 | ) : GraphQLInvocation { 15 | override fun invoke(data: GraphQLInvocation.Data, request: WebRequest): CompletableFuture = 16 | gql.executeAsync( 17 | ExecutionInput.newExecutionInput() 18 | .query(data.query) 19 | .operationName(data.operationName) 20 | .variables(data.variables ?: mapOf()) 21 | .build() 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/internal/InternalWiringFactory.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.internal 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.annotation.AnnotationResolver 4 | import com.auritylab.graphql.kotlin.toolkit.spring.provided.ProvidedScalars 5 | import graphql.schema.DataFetcher 6 | import graphql.schema.GraphQLScalarType 7 | import graphql.schema.TypeResolver 8 | import graphql.schema.idl.FieldWiringEnvironment 9 | import graphql.schema.idl.InterfaceWiringEnvironment 10 | import graphql.schema.idl.ScalarWiringEnvironment 11 | import graphql.schema.idl.UnionWiringEnvironment 12 | import graphql.schema.idl.WiringFactory 13 | import org.springframework.context.annotation.Configuration 14 | 15 | /** 16 | * Describes a [WiringFactory] which resolves types using [AnnotationResolver]. 17 | */ 18 | @Configuration 19 | class InternalWiringFactory( 20 | private val annotationResolver: AnnotationResolver 21 | ) : WiringFactory { 22 | private val providedScalars = mapOf(Pair("Upload", ProvidedScalars.upload)) 23 | 24 | override fun getDataFetcher(environment: FieldWiringEnvironment): DataFetcher<*> = 25 | annotationResolver.getResolver(environment)!! 26 | 27 | override fun getScalar(environment: ScalarWiringEnvironment): GraphQLScalarType = 28 | providedScalars.get(environment.scalarTypeDefinition.name) 29 | ?: annotationResolver.getScalar(environment)!! 30 | 31 | override fun getTypeResolver(environment: InterfaceWiringEnvironment): TypeResolver = 32 | annotationResolver.getTypeResolver(environment)!! 33 | 34 | override fun getTypeResolver(environment: UnionWiringEnvironment): TypeResolver = 35 | annotationResolver.getTypeResolver(environment)!! 36 | 37 | override fun providesScalar(environment: ScalarWiringEnvironment): Boolean = 38 | providedScalars.containsKey(environment.scalarTypeDefinition.name) || 39 | annotationResolver.getScalar(environment) != null 40 | 41 | override fun providesTypeResolver(environment: InterfaceWiringEnvironment): Boolean = 42 | annotationResolver.getTypeResolver(environment) != null 43 | 44 | override fun providesTypeResolver(environment: UnionWiringEnvironment): Boolean = 45 | annotationResolver.getTypeResolver(environment) != null 46 | 47 | override fun providesDataFetcher(environment: FieldWiringEnvironment): Boolean = 48 | annotationResolver.getResolver(environment) != null 49 | } 50 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/provided/ProvidedScalars.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.provided 2 | 3 | import graphql.schema.Coercing 4 | import graphql.schema.GraphQLScalarType 5 | import org.springframework.web.multipart.MultipartFile 6 | 7 | object ProvidedScalars { 8 | /** 9 | * The "Upload" scalar with the according [Coercing] implementation. The coercing will always convert into an 10 | * empty string, as the uploaded file will not be transferred through the variables. 11 | */ 12 | val upload = GraphQLScalarType.newScalar() 13 | .name("Upload") 14 | .coercing( 15 | object : Coercing { 16 | override fun parseValue(input: Any?): MultipartFile? { 17 | // We can only parse if we got a MultipartFile. 18 | if (input is MultipartFile) { 19 | return input 20 | } 21 | 22 | // By default return null. 23 | return null 24 | } 25 | 26 | override fun parseLiteral(input: Any?): MultipartFile? = null 27 | override fun serialize(dataFetcherResult: Any?): MultipartFile? = null 28 | } 29 | ).build() 30 | } 31 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/schema/BaseSchemaAugmentation.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.schema 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.schema.pagination.PaginationSchemaAugmentation 4 | import graphql.schema.GraphQLSchema 5 | 6 | class BaseSchemaAugmentation { 7 | private val delegates = listOf(PaginationSchemaAugmentation()) 8 | 9 | fun augmentSchema(schema: GraphQLSchema): GraphQLSchema { 10 | 11 | var cSchema = schema 12 | var cBuilder = GraphQLSchema.newSchema(cSchema) 13 | 14 | delegates.forEach { 15 | it.augmentSchema(cSchema, cBuilder) 16 | 17 | cSchema = cBuilder.build() 18 | cBuilder = GraphQLSchema.newSchema(cSchema) 19 | } 20 | 21 | cSchema = cBuilder.build() 22 | 23 | return cSchema 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/schema/SchemaAugmentation.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.schema 2 | 3 | import graphql.schema.GraphQLSchema 4 | 5 | interface SchemaAugmentation { 6 | fun augmentSchema(existingSchema: GraphQLSchema, transform: GraphQLSchema.Builder) 7 | } 8 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/schema/SchemaTypeGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.schema 2 | 3 | import graphql.schema.GraphQLType 4 | 5 | interface SchemaTypeGenerator { 6 | fun generateTypes(): Collection 7 | } 8 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/schema/pagination/PaginationPageInfoTypeGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.schema.pagination 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.schema.SchemaTypeGenerator 4 | import graphql.Scalars 5 | import graphql.schema.GraphQLObjectType 6 | import graphql.schema.GraphQLType 7 | 8 | class PaginationPageInfoTypeGenerator : SchemaTypeGenerator { 9 | override fun generateTypes(): Collection { 10 | return listOf( 11 | GraphQLObjectType.newObject() 12 | .name("PageInfo") 13 | .field { 14 | it.name("hasPreviousPage") 15 | it.type(Scalars.GraphQLBoolean) 16 | } 17 | .field { 18 | it.name("hasNextPage") 19 | it.type(Scalars.GraphQLBoolean) 20 | } 21 | .field { 22 | it.name("startCursor") 23 | it.type(Scalars.GraphQLString) 24 | } 25 | .field { 26 | it.name("endCursor") 27 | it.type(Scalars.GraphQLString) 28 | } 29 | .build() 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/schema/pagination/PaginationSchemaAugmentation.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.schema.pagination 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.common.directive.DirectiveFacade 4 | import com.auritylab.graphql.kotlin.toolkit.common.helper.GraphQLTypeHelper 5 | import com.auritylab.graphql.kotlin.toolkit.spring.schema.SchemaAugmentation 6 | import graphql.Scalars 7 | import graphql.schema.GraphQLArgument 8 | import graphql.schema.GraphQLFieldDefinition 9 | import graphql.schema.GraphQLNamedType 10 | import graphql.schema.GraphQLObjectType 11 | import graphql.schema.GraphQLOutputType 12 | import graphql.schema.GraphQLSchema 13 | import graphql.schema.GraphQLType 14 | import graphql.schema.GraphQLTypeReference 15 | 16 | class PaginationSchemaAugmentation : SchemaAugmentation { 17 | override fun augmentSchema(existingSchema: GraphQLSchema, transform: GraphQLSchema.Builder) { 18 | 19 | val paginatedTypes = mutableListOf() 20 | 21 | val augmentedTypes = existingSchema.additionalTypes 22 | .map { type -> 23 | if (type !is GraphQLObjectType) 24 | return@map type 25 | 26 | val result = mapObjectType(type) 27 | paginatedTypes.addAll(result.second) 28 | result.first 29 | } 30 | 31 | transform.query(mapObjectType(existingSchema.queryType).let { paginatedTypes.addAll(it.second); it.first }) 32 | transform.mutation(mapObjectType(existingSchema.mutationType).let { paginatedTypes.addAll(it.second); it.first }) 33 | 34 | transform.clearAdditionalTypes() 35 | transform.additionalTypes(augmentedTypes.toSet()) 36 | 37 | if (paginatedTypes.isNotEmpty()) { 38 | transform.additionalTypes(PaginationPageInfoTypeGenerator().generateTypes().toSet()) 39 | 40 | paginatedTypes.forEach { type -> 41 | if (type is GraphQLObjectType) 42 | transform.additionalTypes(PaginationTypesGenerator(type).generateTypes().toSet()) 43 | } 44 | } 45 | } 46 | 47 | private fun getFieldDefinitions(schema: GraphQLSchema) = 48 | schema.allTypesAsList 49 | .filterIsInstance() 50 | .flatMap { it.fieldDefinitions } 51 | 52 | private fun getMatchingFieldDefinitions( 53 | definitions: Collection 54 | ): Collection = 55 | definitions.filter { DirectiveFacade.Defaults.pagination[it] } 56 | 57 | private fun getConnectionType(input: GraphQLType): GraphQLOutputType { 58 | if (input !is GraphQLNamedType) 59 | throw IllegalArgumentException("Expected named type") 60 | return GraphQLTypeReference(input.name + "Connection") 61 | } 62 | 63 | private fun mapObjectType(type: GraphQLObjectType): Pair> { 64 | val paginationTypes = mutableListOf() 65 | 66 | return Pair( 67 | type.transform { trans -> 68 | val augmentedFields = type.fieldDefinitions.map { field -> 69 | if (!DirectiveFacade.Defaults.pagination[field]) 70 | field 71 | else { 72 | val unwrappedType = GraphQLTypeHelper.unwrapType(field.type) 73 | 74 | paginationTypes.add(unwrappedType) 75 | field.transform { 76 | it.arguments(field.arguments.plus(buildPaginationArguments())) 77 | it.type(getConnectionType(unwrappedType)) 78 | } 79 | } 80 | } 81 | 82 | trans.clearFields() 83 | trans.fields(augmentedFields) 84 | }, 85 | paginationTypes 86 | ) 87 | } 88 | 89 | private fun buildPaginationArguments(): List { 90 | return listOf( 91 | GraphQLArgument.newArgument().name("first").type(Scalars.GraphQLInt).build(), 92 | GraphQLArgument.newArgument().name("after").type(Scalars.GraphQLString).build(), 93 | GraphQLArgument.newArgument().name("last").type(Scalars.GraphQLInt).build(), 94 | GraphQLArgument.newArgument().name("before").type(Scalars.GraphQLString).build() 95 | ) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/schema/pagination/PaginationTypesGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.schema.pagination 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.schema.SchemaTypeGenerator 4 | import graphql.Scalars 5 | import graphql.schema.GraphQLList 6 | import graphql.schema.GraphQLObjectType 7 | import graphql.schema.GraphQLType 8 | import graphql.schema.GraphQLTypeReference 9 | 10 | class PaginationTypesGenerator( 11 | private val objectType: GraphQLObjectType 12 | ) : SchemaTypeGenerator { 13 | override fun generateTypes(): Collection { 14 | return listOf( 15 | buildConnectionType(), 16 | buildEdgeType() 17 | ) 18 | } 19 | 20 | private fun buildConnectionType(): GraphQLObjectType { 21 | return GraphQLObjectType.newObject() 22 | .name(connectionTypeName) 23 | .field { 24 | it.name("pageInfo") 25 | it.type(GraphQLTypeReference("PageInfo")) 26 | } 27 | .field { 28 | it.name("edges") 29 | it.type(GraphQLList(GraphQLTypeReference(edgeTypeName))) 30 | } 31 | .build() 32 | } 33 | 34 | private fun buildEdgeType(): GraphQLObjectType { 35 | return GraphQLObjectType.newObject() 36 | .name(edgeTypeName) 37 | .field { 38 | it.name("node") 39 | it.type(objectType) 40 | } 41 | .field { 42 | it.name("cursor") 43 | it.type(Scalars.GraphQLString) 44 | } 45 | .build() 46 | } 47 | 48 | private val connectionTypeName = objectType.name + "Connection" 49 | private val edgeTypeName = objectType.name + "Edge" 50 | } 51 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.auritylab.graphql.kotlin.toolkit.spring.AutoConfiguration 2 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/SyncGQLInvocation.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.api.GraphQLInvocation 4 | import graphql.ExecutionInput 5 | import graphql.ExecutionResult 6 | import graphql.GraphQL 7 | import org.springframework.web.context.request.WebRequest 8 | import java.util.concurrent.CompletableFuture 9 | 10 | class SyncGQLInvocation( 11 | private val gql: GraphQL 12 | ) : GraphQLInvocation { 13 | override fun invoke(data: GraphQLInvocation.Data, request: WebRequest): CompletableFuture { 14 | return CompletableFuture.completedFuture( 15 | gql.execute( 16 | ExecutionInput.newExecutionInput() 17 | .query(data.query) 18 | .operationName(data.operationName) 19 | .variables(data.variables ?: mapOf()) 20 | .build() 21 | ) 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/TestConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.api.schemaOfResourceFiles 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import com.fasterxml.jackson.module.kotlin.KotlinModule 6 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration 7 | import org.springframework.context.annotation.Bean 8 | import org.springframework.context.annotation.Configuration 9 | import org.springframework.context.annotation.Import 10 | import org.springframework.web.servlet.config.annotation.EnableWebMvc 11 | 12 | @Configuration 13 | @EnableWebMvc 14 | @EnableAutoConfiguration 15 | @Import(AutoConfiguration::class) 16 | internal class TestConfiguration { 17 | @Bean 18 | fun objectMapper(): ObjectMapper = ObjectMapper().registerModule(KotlinModule()) 19 | 20 | @Bean 21 | fun schema() = schemaOfResourceFiles("schemas/schema.graphqls") 22 | } 23 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/TestOperations.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring 2 | 3 | import me.lazmaid.kraph.Kraph 4 | 5 | object TestOperations { 6 | val getUserQuery = Kraph { 7 | query { 8 | fieldObject("getUser") { 9 | field("id") 10 | field("name") 11 | field("surname") 12 | } 13 | } 14 | }.toGraphQueryString() 15 | 16 | val createUserMutation_withoutUpload = Kraph { 17 | mutation { 18 | // val uploadVar = variable("upload", "Upload", "") 19 | fieldObject("createUser", args = mapOf("name" to "test", "surname" to "test")) { 20 | field("id") 21 | field("name") 22 | field("surname") 23 | } 24 | } 25 | }.toGraphQueryString() 26 | 27 | val createUserMutation_withUpload = Kraph { 28 | mutation { 29 | val uploadVar = variable("upload", "Upload", "") 30 | fieldObject("createUser", args = mapOf("name" to "test", "surname" to "test", "upload" to uploadVar)) { 31 | field("id") 32 | field("name") 33 | field("surname") 34 | } 35 | } 36 | }.toGraphQueryString() 37 | } 38 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/api/GraphQLSchemaSupplierExtensionsTest.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.api 2 | 3 | import org.junit.jupiter.api.Assertions.* 4 | import org.junit.jupiter.api.Test 5 | import org.junit.jupiter.api.assertThrows 6 | 7 | internal class GraphQLSchemaSupplierExtensionsTest { 8 | @Test 9 | fun `should create supplier based on string collection`() { 10 | val supplier = schemaOfStrings(setOf("test1", "test2")) 11 | 12 | assertEquals(2, supplier.schemas.size) 13 | } 14 | 15 | @Test 16 | fun `should create supplier based on string varargs`() { 17 | val supplier = schemaOfStrings("test1", "test2") 18 | 19 | assertEquals(2, supplier.schemas.size) 20 | } 21 | 22 | @Test 23 | fun `should load schema from resource files by collection`() { 24 | val supplier = schemaOfResourceFiles(setOf("schemas/schema.graphqls")) 25 | 26 | assertEquals(1, supplier.schemas.size) 27 | } 28 | 29 | @Test 30 | fun `should load schema from resource files by varargs`() { 31 | val supplier = schemaOfResourceFiles("schemas/schema.graphqls") 32 | 33 | assertEquals(1, supplier.schemas.size) 34 | } 35 | 36 | @Test 37 | fun `should throw exception if resource file could not be found by collection`() { 38 | assertThrows { 39 | schemaOfResourceFiles(setOf("notfound.graphqls")) 40 | } 41 | } 42 | 43 | @Test 44 | fun `should throw exception if resource file could not be found by vararg`() { 45 | assertThrows { 46 | schemaOfResourceFiles("notfound.graphqls") 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/controller/AbstractControllerTest.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.controller 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.TestConfiguration 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc 7 | import org.springframework.test.annotation.DirtiesContext 8 | import org.springframework.test.context.ContextConfiguration 9 | import org.springframework.test.web.servlet.MockMvc 10 | 11 | /** 12 | * Implements the abstract for all controller tests. This provides some based beans and utility functions to 13 | * simplify work with the operations. 14 | */ 15 | @DirtiesContext 16 | @AutoConfigureMockMvc 17 | @ContextConfiguration(classes = [TestConfiguration::class]) 18 | internal abstract class AbstractControllerTest { 19 | @Autowired 20 | protected lateinit var mvc: MockMvc 21 | 22 | @Autowired 23 | protected lateinit var objectMapper: ObjectMapper 24 | 25 | /** 26 | * Will create a JSON encoded GraphQL body. The [query] must be given, [operationName] and [variables] are optional. 27 | */ 28 | protected fun body(query: String, operationName: String?, variables: Map?): String { 29 | val map = mutableMapOf() 30 | 31 | map["query"] = query 32 | 33 | operationName 34 | ?.let { map["operationName"] = it } 35 | 36 | variables 37 | ?.let { map["variables"] = it } 38 | 39 | return objectMapper.writeValueAsString(map) 40 | } 41 | 42 | /** 43 | * Will return a [ByteArray] which contains a file for testing purpose. 44 | */ 45 | protected fun file(): ByteArray = 46 | javaClass.classLoader.getResourceAsStream("test_file.png")!!.readAllBytes() 47 | } 48 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/controller/AbstractDataFetcherControllerTest.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.controller 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.SyncGQLInvocation 4 | import com.auritylab.graphql.kotlin.toolkit.spring.api.GraphQLInvocation 5 | import com.auritylab.graphql.kotlin.toolkit.spring.provided.ProvidedScalars 6 | import com.nhaarman.mockitokotlin2.any 7 | import com.nhaarman.mockitokotlin2.whenever 8 | import graphql.GraphQL 9 | import graphql.schema.DataFetcher 10 | import graphql.schema.GraphQLScalarType 11 | import graphql.schema.idl.FieldWiringEnvironment 12 | import graphql.schema.idl.ScalarWiringEnvironment 13 | import graphql.schema.idl.WiringFactory 14 | import org.mockito.Mockito 15 | import org.springframework.beans.factory.annotation.Autowired 16 | import org.springframework.beans.factory.annotation.Qualifier 17 | import org.springframework.boot.test.context.TestConfiguration 18 | import org.springframework.context.annotation.Bean 19 | import org.springframework.context.annotation.Primary 20 | import org.springframework.context.annotation.Profile 21 | import org.springframework.test.context.ActiveProfiles 22 | 23 | /** 24 | * Abstract implementation of a controller test which verifies against a data fetcher. This will create a single 25 | * mocked [DataFetcher] bean with the according [WiringFactory]. The mocked [DataFetcher] can be obtained through 26 | * [dataFetcher]. 27 | */ 28 | @ActiveProfiles("data-fetcher-test") 29 | internal abstract class AbstractDataFetcherControllerTest : AbstractControllerTest() { 30 | @TestConfiguration 31 | class Configuration { 32 | @Bean 33 | @Primary 34 | @Profile("data-fetcher-test") 35 | fun syncInvocation(gql: GraphQL): GraphQLInvocation = SyncGQLInvocation(gql) 36 | 37 | @Bean() 38 | fun dataFetcher(): DataFetcher<*> { 39 | val m = Mockito.mock(DataFetcher::class.java) 40 | 41 | // Always return null. 42 | whenever(m.get(any())).then { 43 | null 44 | } 45 | 46 | return m 47 | } 48 | 49 | @Bean 50 | @Primary 51 | fun customWiringFactory(@Qualifier("dataFetcher") df: DataFetcher<*>): WiringFactory { 52 | return object : WiringFactory { 53 | override fun providesScalar(environment: ScalarWiringEnvironment): Boolean { 54 | // For testing purpose we need to add the Upload scalar. 55 | return environment.scalarTypeDefinition.name == "Upload" 56 | } 57 | 58 | override fun getScalar(environment: ScalarWiringEnvironment): GraphQLScalarType { 59 | if (environment.scalarTypeDefinition.name == "Upload") 60 | return ProvidedScalars.upload 61 | 62 | throw IllegalStateException() 63 | } 64 | 65 | override fun providesDataFetcher(environment: FieldWiringEnvironment): Boolean { 66 | // As we cover all tests with the createUser mutation, we just simply the DataFetcher for this field. 67 | return environment.fieldDefinition.name == "createUser" 68 | } 69 | 70 | override fun getDataFetcher(environment: FieldWiringEnvironment): DataFetcher<*> { 71 | if (environment.fieldDefinition.name == "createUser") 72 | return df 73 | 74 | throw IllegalStateException() 75 | } 76 | } 77 | } 78 | } 79 | 80 | @Autowired 81 | @Qualifier("dataFetcher") 82 | lateinit var dataFetcher: DataFetcher<*> 83 | } 84 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/controller/AbstractInvocationControllerTest.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.controller 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.api.GraphQLInvocation 4 | import com.nhaarman.mockitokotlin2.any 5 | import com.nhaarman.mockitokotlin2.clearInvocations 6 | import com.nhaarman.mockitokotlin2.whenever 7 | import graphql.ExecutionResultImpl 8 | import org.junit.jupiter.api.BeforeEach 9 | import org.mockito.Mockito 10 | import org.mockito.internal.util.MockUtil 11 | import org.springframework.beans.factory.annotation.Autowired 12 | import org.springframework.boot.test.context.TestConfiguration 13 | import org.springframework.context.annotation.Bean 14 | import org.springframework.context.annotation.Primary 15 | import org.springframework.context.annotation.Profile 16 | import org.springframework.test.context.ActiveProfiles 17 | import java.util.concurrent.CompletableFuture 18 | 19 | /** 20 | * Abstract implementation of a controller test which verifies against the invocation. This will create a mocked 21 | * [GraphQLInvocation] bean. The mocked instance can be obtained with [invocation]. The invocations will rest 22 | * before each test. 23 | */ 24 | @ActiveProfiles("invocation-test") 25 | internal abstract class AbstractInvocationControllerTest : AbstractControllerTest() { 26 | @TestConfiguration 27 | class Configuration { 28 | @Bean 29 | @Primary 30 | @Profile("invocation-test") 31 | fun mockedInvocation(): GraphQLInvocation { 32 | val m = Mockito.mock(GraphQLInvocation::class.java) 33 | 34 | whenever( 35 | m.invoke( 36 | any(), 37 | any() 38 | ) 39 | ).thenReturn(CompletableFuture.completedFuture(ExecutionResultImpl(listOf()))) 40 | 41 | return m 42 | } 43 | } 44 | 45 | @Autowired 46 | protected lateinit var invocation: GraphQLInvocation 47 | 48 | @BeforeEach 49 | fun resetMock() { 50 | // Just to be sure the instance is the mock. 51 | if (MockUtil.isMock(invocation)) 52 | clearInvocations(invocation) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/controller/GetControllerTest.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.controller 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.TestOperations 4 | import com.nhaarman.mockitokotlin2.any 5 | import com.nhaarman.mockitokotlin2.argThat 6 | import com.nhaarman.mockitokotlin2.times 7 | import com.nhaarman.mockitokotlin2.verify 8 | import org.junit.jupiter.api.Test 9 | import org.springframework.boot.test.context.SpringBootTest 10 | import org.springframework.test.web.servlet.get 11 | import java.util.UUID 12 | 13 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 14 | internal class GetControllerTest : AbstractInvocationControllerTest() { 15 | @Test 16 | fun `(get) should call invocation correctly`() { 17 | val inputQuery = TestOperations.getUserQuery 18 | 19 | mvc.get("/graphql") { 20 | param("query", inputQuery) 21 | }.andExpect { status { isOk } } 22 | 23 | // Invocation shall be called exactly once with the input query. 24 | verify(invocation, times(1)) 25 | .invoke(argThat { query == inputQuery }, any()) 26 | } 27 | 28 | @Test 29 | fun `(get) should call invocation with variables correctly`() { 30 | val inputQuery = TestOperations.getUserQuery 31 | val inputVariables = mapOf(Pair("name", "test"), Pair("surname", "test")) 32 | 33 | mvc.get("/graphql") { 34 | param("query", inputQuery) 35 | param("variables", objectMapper.writeValueAsString(inputVariables)) 36 | }.andExpect { status { isOk } } 37 | 38 | // Invocation shall be called exactly once with the input variables. 39 | verify(invocation, times(1)) 40 | .invoke(argThat { variables == inputVariables }, any()) 41 | } 42 | 43 | @Test 44 | fun `(get) should call invocation with operation name correctly`() { 45 | val inputQuery = TestOperations.getUserQuery 46 | val inputOperationName = UUID.randomUUID().toString() 47 | 48 | mvc.get("/graphql") { 49 | param("query", inputQuery) 50 | param("operationName", inputOperationName) 51 | }.andExpect { status { isOk } } 52 | 53 | // Invocation shall be called exactly once with the input operation name. 54 | verify(invocation, times(1)) 55 | .invoke(argThat { operationName == inputOperationName }, any()) 56 | } 57 | 58 | @Test 59 | fun `(get) should throw error when query param is not given`() { 60 | mvc.get("/graphql") 61 | .andExpect { status { `is`(400) } } 62 | } 63 | 64 | @Test 65 | fun `(get) should handle nested variables correctly`() { 66 | val inputQuery = TestOperations.getUserQuery 67 | val inputVariables = mapOf( 68 | Pair("name", "test"), 69 | Pair("surname", "test"), 70 | Pair("meta", mapOf(Pair("surname", "true"), Pair("name", "false"))) 71 | ) 72 | 73 | mvc.get("/graphql") { 74 | param("query", inputQuery) 75 | param("variables", objectMapper.writeValueAsString(inputVariables)) 76 | }.andExpect { status { isOk } } 77 | 78 | // Invocation shall be called exactly once with the input variables. 79 | verify(invocation, times(1)) 80 | .invoke(argThat { variables == inputVariables }, any()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/controller/PostControllerTest.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.controller 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.TestOperations 4 | import com.nhaarman.mockitokotlin2.any 5 | import com.nhaarman.mockitokotlin2.argThat 6 | import com.nhaarman.mockitokotlin2.times 7 | import com.nhaarman.mockitokotlin2.verify 8 | import org.junit.jupiter.api.Test 9 | import org.springframework.boot.test.context.SpringBootTest 10 | import org.springframework.http.MediaType 11 | import org.springframework.test.web.servlet.post 12 | import java.util.UUID 13 | 14 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 15 | internal class PostControllerTest : AbstractInvocationControllerTest() { 16 | @Test 17 | fun `(post) should call invocation on application-json body correctly`() { 18 | val inputQuery = TestOperations.getUserQuery 19 | val inputOperation = UUID.randomUUID().toString() 20 | val inputVariables = mapOf(Pair("name", "test"), Pair("surname", "test")) 21 | val inputContent = body(inputQuery, inputOperation, inputVariables) 22 | 23 | mvc.post("/graphql") { 24 | content = inputContent 25 | contentType = MediaType.APPLICATION_JSON 26 | }.andExpect { status { isOk } } 27 | 28 | verify(invocation, times(1)) 29 | .invoke( 30 | argThat { 31 | query == inputQuery && 32 | operationName == inputOperation && 33 | variables == inputVariables 34 | }, 35 | any() 36 | ) 37 | } 38 | 39 | @Test 40 | fun `(post) should call invocation on application-graphql body correctly`() { 41 | val inputQuery = TestOperations.getUserQuery 42 | 43 | mvc.post("/graphql") { 44 | content = inputQuery 45 | contentType = MediaType.parseMediaType("application/graphql") 46 | }.andExpect { status { isOk } } 47 | 48 | verify(invocation, times(1)) 49 | .invoke( 50 | argThat { 51 | query == inputQuery 52 | }, 53 | any() 54 | ) 55 | } 56 | 57 | @Test 58 | fun `(post) should call invocation on query parameter correctly`() { 59 | val inputQuery = TestOperations.getUserQuery 60 | 61 | mvc.post("/graphql") { 62 | param("query", inputQuery) 63 | contentType = MediaType.TEXT_PLAIN 64 | }.andExpect { status { isOk } } 65 | 66 | verify(invocation, times(1)) 67 | .invoke( 68 | argThat { 69 | query == inputQuery 70 | }, 71 | any() 72 | ) 73 | } 74 | 75 | @Test 76 | fun `(post) should handle nested variables correctly`() { 77 | val inputQuery = TestOperations.getUserQuery 78 | val inputOperation = UUID.randomUUID().toString() 79 | val inputVariables = mapOf( 80 | Pair("name", "test"), 81 | Pair("surname", "test"), 82 | Pair("meta", mapOf(Pair("surname", "true"), Pair("name", "false"))) 83 | ) 84 | val inputContent = body(inputQuery, inputOperation, inputVariables) 85 | 86 | mvc.post("/graphql") { 87 | content = inputContent 88 | contentType = MediaType.APPLICATION_JSON 89 | }.andExpect { status { isOk } } 90 | 91 | verify(invocation, times(1)) 92 | .invoke( 93 | argThat { 94 | query == inputQuery && 95 | operationName == inputOperation && 96 | variables == inputVariables 97 | }, 98 | any() 99 | ) 100 | } 101 | 102 | @Test 103 | fun `(post) should handle invalid request body properly`() { 104 | mvc.post("/graphql") { 105 | content = "invalid json..." 106 | contentType = MediaType.APPLICATION_JSON 107 | }.andExpect { 108 | // Expect an unprocessable entity status because the entity couldn't be parsed... 109 | status { isUnprocessableEntity } 110 | } 111 | } 112 | 113 | @Test 114 | fun `(post) should handle empty post request properly`() { 115 | mvc.post("/graphql").andExpect { status { isBadRequest } } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/controller/UploadControllerTest.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.controller 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.TestOperations 4 | import com.nhaarman.mockitokotlin2.any 5 | import com.nhaarman.mockitokotlin2.check 6 | import com.nhaarman.mockitokotlin2.times 7 | import com.nhaarman.mockitokotlin2.verify 8 | import org.junit.jupiter.api.Assertions.assertEquals 9 | import org.junit.jupiter.api.Test 10 | import org.springframework.boot.test.context.SpringBootTest 11 | import org.springframework.test.web.servlet.multipart 12 | import org.springframework.web.multipart.MultipartFile 13 | 14 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 15 | internal class UploadControllerTest : AbstractInvocationControllerTest() { 16 | @Test 17 | fun `(post multipart) should call invocation correctly`() { 18 | val inputFileZero = file() 19 | val inputQuery = TestOperations.createUserMutation_withUpload 20 | val inputOperation = body(inputQuery, null, mapOf(Pair("upload", null))) 21 | val inputMap = objectMapper.writeValueAsString(mapOf(Pair("0", listOf("variables.upload")))) 22 | 23 | mvc.multipart("/graphql") { 24 | param("operations", inputOperation) 25 | param("map", inputMap) 26 | file("0", inputFileZero) 27 | }.andExpect { status { isOk } } 28 | 29 | verify(invocation, times(1)).invoke( 30 | check { 31 | assertEquals(inputQuery, it.query) 32 | assertEquals(inputFileZero, (it.variables!!["upload"] as MultipartFile).bytes) 33 | }, 34 | any() 35 | ) 36 | } 37 | 38 | @Test 39 | fun `(post multipart) should handle invalid operation properly`() { 40 | val inputMap = objectMapper.writeValueAsString(mapOf(Pair("0", listOf("variables.upload")))) 41 | 42 | mvc.multipart("/graphql") { 43 | param("operations", "invalid...") 44 | param("map", inputMap) 45 | file("0", file()) 46 | }.andExpect { 47 | status { isUnprocessableEntity } 48 | } 49 | } 50 | 51 | @Test 52 | fun `(post multipart) should handle invalid map properly`() { 53 | val inputQuery = TestOperations.createUserMutation_withUpload 54 | val inputOperation = body(inputQuery, null, mapOf(Pair("upload", null))) 55 | 56 | mvc.multipart("/graphql") { 57 | // Operations must be valid for this test case... 58 | param("operations", inputOperation) 59 | param("map", "invalid...") 60 | file("0", file()) 61 | }.andExpect { status { isUnprocessableEntity } } 62 | } 63 | 64 | @Test 65 | fun `(post multipart) should handle non-existing file properly` () { 66 | val inputQuery = TestOperations.createUserMutation_withUpload 67 | val inputOperation = body(inputQuery, null, mapOf(Pair("upload", null))) 68 | val inputMap = objectMapper.writeValueAsString(mapOf(Pair("0", listOf("variables.upload")))) 69 | 70 | mvc.multipart("/graphql") { 71 | param("operations", inputOperation) 72 | param("map", inputMap) 73 | // No file, because we expect it to fail... 74 | }.andExpect { 75 | status {isBadRequest} 76 | } 77 | } 78 | 79 | @Test 80 | fun `(post multipart) should handle invalid path properly` () { 81 | val inputQuery = TestOperations.createUserMutation_withUpload 82 | val inputOperation = body(inputQuery, null, mapOf(Pair("upload", null))) 83 | val inputMap = objectMapper.writeValueAsString(mapOf(Pair("0", listOf("v.u")))) 84 | 85 | mvc.multipart("/graphql") { 86 | param("operations", inputOperation) 87 | param("map", inputMap) 88 | file("0", file()) 89 | }.andExpect { 90 | status {isBadRequest} 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/spring/controller/UploadDataFetcherControllerTest.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.spring.controller 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.spring.TestOperations 4 | import com.nhaarman.mockitokotlin2.times 5 | import com.nhaarman.mockitokotlin2.verify 6 | import org.junit.jupiter.api.Assertions.assertNotNull 7 | import org.junit.jupiter.api.Assertions.assertTrue 8 | import org.junit.jupiter.api.Test 9 | import org.springframework.boot.test.context.SpringBootTest 10 | import org.springframework.test.web.servlet.multipart 11 | import org.springframework.web.multipart.MultipartFile 12 | 13 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 14 | internal class UploadDataFetcherControllerTest : AbstractDataFetcherControllerTest() { 15 | @Test 16 | fun `(post multipart) should pass file upload to resolver correctly`() { 17 | val inputFileZero = file() 18 | val inputMutation = TestOperations.createUserMutation_withUpload 19 | val inputOperation = body(inputMutation, null, mapOf(Pair("upload", null))) 20 | val inputMap = objectMapper.writeValueAsString(mapOf(Pair("0", listOf("variables.upload")))) 21 | 22 | mvc.multipart("/graphql") { 23 | param("operations", inputOperation) 24 | param("map", inputMap) 25 | file("0", inputFileZero) 26 | }.andExpect { status { isOk } } 27 | 28 | verify(dataFetcher, times(1)).get( 29 | com.nhaarman.mockitokotlin2.check { 30 | assertTrue(it.containsArgument("name")) 31 | assertTrue(it.containsArgument("surname")) 32 | assertTrue(it.containsArgument("upload")) 33 | 34 | assertNotNull(it.getArgument("name")) 35 | assertNotNull(it.getArgument("surname")) 36 | assertNotNull(it.getArgument("upload")) 37 | 38 | assertTrue(it.getArgument("name") is String) 39 | assertTrue(it.getArgument("surname") is String) 40 | assertTrue(it.getArgument("upload") is MultipartFile) 41 | } 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/test/resources/schemas/schema.graphqls: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | mutation: Mutation 4 | } 5 | 6 | type Query { 7 | getUser: User 8 | } 9 | 10 | type Mutation { 11 | createUser(name: String!, surname: String!, upload: Upload): User 12 | } 13 | 14 | type User { 15 | id: ID 16 | name: String 17 | surname: String 18 | } 19 | 20 | # Simple definition of the Upload scalar. 21 | scalar Upload 22 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-spring-boot/src/test/resources/test_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AurityLab/graphql-kotlin-toolkit/3dbb4e3b43cf713da34297bf827b63caaeb15ffa/graphql-kotlin-toolkit-spring-boot/src/test/resources/test_file.png -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-util/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.kapt") 3 | } 4 | 5 | ext { 6 | this["publication.enabled"] = true 7 | this["publication.artifactId"] = "util" 8 | this["publication.name"] = "GraphQL Kotlin Toolkit: Util" 9 | this["publication.description"] = "Util" 10 | } 11 | 12 | dependencies { 13 | implementation(project(":graphql-kotlin-toolkit-common")) 14 | implementation(project(":graphql-kotlin-toolkit-codegen-binding")) 15 | 16 | // JPA API. 17 | compileOnly("jakarta.persistence:jakarta.persistence-api:2.2.3") 18 | 19 | // GraphQL-Java dependency. 20 | implementation("com.graphql-java:graphql-java:16.2") 21 | 22 | // Test dependencies. 23 | testImplementation("com.github.VerachadW:kraph:v.0.6.1") 24 | testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0") 25 | } 26 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-util/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/util/selection/EnhancedDataFetchingFieldSelectionSet.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.util.selection 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegenbinding.types.MetaFieldsContainer 4 | import com.auritylab.graphql.kotlin.toolkit.common.markers.Experimental 5 | import graphql.schema.DataFetchingFieldSelectionSet 6 | import graphql.schema.SelectedField 7 | 8 | /** 9 | * Enhanced [DataFetchingFieldSelectionSet] which provides the [MetaFieldsContainer]. Basically it's just a proxy. 10 | * 11 | * @param delegate The object to which the calls will be delegated. 12 | * @param type Object of the [MetaFieldsContainer] -> [T]. 13 | * @param T Type of the [MetaFieldsContainer] which is represented by this SelectionSet. 14 | */ 15 | @Experimental 16 | class EnhancedDataFetchingFieldSelectionSet>( 17 | private val delegate: DataFetchingFieldSelectionSet, 18 | val type: T 19 | ) : DataFetchingFieldSelectionSet { 20 | 21 | override fun contains(fieldGlobPattern: String?): Boolean { 22 | return delegate.contains(fieldGlobPattern) 23 | } 24 | 25 | override fun containsAnyOf(fieldGlobPattern: String?, vararg fieldGlobPatterns: String?): Boolean { 26 | return delegate.containsAnyOf(fieldGlobPattern, *fieldGlobPatterns) 27 | } 28 | 29 | override fun containsAllOf(fieldGlobPattern: String?, vararg fieldGlobPatterns: String?): Boolean { 30 | return delegate.containsAllOf(fieldGlobPattern, *fieldGlobPatterns) 31 | } 32 | 33 | override fun getFields(): MutableList { 34 | return delegate.fields 35 | } 36 | 37 | override fun getFields(fieldGlobPattern: String?, vararg fieldGlobPatterns: String?): MutableList { 38 | return delegate.getFields(fieldGlobPattern) 39 | } 40 | 41 | override fun getImmediateFields(): MutableList { 42 | return delegate.immediateFields 43 | } 44 | 45 | override fun getFieldsGroupedByResultKey(): MutableMap> { 46 | return delegate.fieldsGroupedByResultKey 47 | } 48 | 49 | override fun getFieldsGroupedByResultKey( 50 | fieldGlobPattern: String?, 51 | vararg fieldGlobPatterns: String? 52 | ): MutableMap> { 53 | return delegate.getFieldsGroupedByResultKey(fieldGlobPattern, *fieldGlobPatterns) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-util/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/util/selection/SelectionSetExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.util.selection 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegenbinding.types.AbstractEnv 4 | import com.auritylab.graphql.kotlin.toolkit.codegenbinding.types.MetaObjectType 5 | import com.auritylab.graphql.kotlin.toolkit.common.markers.Experimental 6 | import com.auritylab.graphql.kotlin.toolkit.util.selection.steps.SelectionSetMultiOutputStep 7 | import com.auritylab.graphql.kotlin.toolkit.util.selection.steps.SelectionSetOutputStep 8 | import com.auritylab.graphql.kotlin.toolkit.util.selection.steps.SelectionSetRootStep 9 | import com.auritylab.graphql.kotlin.toolkit.util.selection.steps.SelectionSetSingleOutputStep 10 | import com.auritylab.graphql.kotlin.toolkit.util.selection.steps.SelectionSetStep 11 | import graphql.schema.SelectedField 12 | 13 | @Experimental 14 | inline fun > EnhancedDataFetchingFieldSelectionSet.getFields(builder: SelectionSetRootStep.() -> SelectionSetMultiOutputStep): List = 15 | getFields(builder(SelectionSetStep.start(type)).buildPattern()) 16 | 17 | @Experimental 18 | inline fun > EnhancedDataFetchingFieldSelectionSet.contains(builder: SelectionSetRootStep.() -> SelectionSetOutputStep): Boolean = 19 | contains(builder(SelectionSetStep.start(type)).buildPattern()) 20 | 21 | @Experimental 22 | val > AbstractEnv<*, *, T>.selectionSet: EnhancedDataFetchingFieldSelectionSet 23 | get() = EnhancedDataFetchingFieldSelectionSet(this.original.selectionSet, this.type) 24 | 25 | @Experimental 26 | inline fun > AbstractEnv<*, *, T>.selectionFields(builder: SelectionSetRootStep.() -> SelectionSetMultiOutputStep): List = 27 | selectionSet.getFields(builder) 28 | 29 | @Experimental 30 | inline fun > AbstractEnv<*, *, T>.selectionContains(builder: SelectionSetRootStep.() -> SelectionSetOutputStep): Boolean = 31 | selectionSet.contains(builder) 32 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-util/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/util/selection/steps/SelectionSetStepImpls.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.util.selection.steps 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegenbinding.types.MetaField 4 | import com.auritylab.graphql.kotlin.toolkit.codegenbinding.types.MetaFieldWithReference 5 | import com.auritylab.graphql.kotlin.toolkit.codegenbinding.types.MetaFieldsContainer 6 | 7 | internal abstract class AbstractSelectionSetFinalizeStep : SelectionSetOutputStep { 8 | override fun buildPattern(): String { 9 | val parts = mutableListOf() 10 | 11 | var current: SelectionSetStep? = this 12 | while (current != null) { 13 | val stringRep = current.string 14 | if (stringRep != null) 15 | parts.add(stringRep) 16 | 17 | current = current.parent 18 | } 19 | 20 | return parts.asReversed().joinToString("/") 21 | } 22 | } 23 | 24 | internal abstract class AbstractSelectionSetFieldsContainerStep>( 25 | private val objectType: T, 26 | private val field: MetaField<*>?, 27 | ) : AbstractSelectionSetFinalizeStep(), SelectionSetFieldsContainerStep { 28 | override fun > fieldRef(resolver: T.() -> MetaFieldWithReference): SelectionSetFieldStep { 29 | val resolved = resolver(objectType) 30 | 31 | return SelectionSetFieldStepImpl(this, resolved.ref, resolved) 32 | } 33 | 34 | override fun field(resolver: T.() -> MetaField<*>): SelectionSetSingleOutputStep { 35 | val resolved = resolver(objectType) 36 | 37 | return SelectionSetOutputFieldStepImpl(this, resolved) 38 | } 39 | 40 | override fun wildcard(): SelectionSetWildcardStep { 41 | return SelectionSetWildcardStepImpl(this) 42 | } 43 | } 44 | 45 | internal class SelectionSetRootStepImpl>(objectType: T) : 46 | AbstractSelectionSetFieldsContainerStep(objectType, null), SelectionSetRootStep 47 | 48 | internal class SelectionSetWildcardStepImpl( 49 | override val parent: SelectionSetStep? 50 | ) : AbstractSelectionSetFinalizeStep(), SelectionSetWildcardStep { 51 | override val string: String = "*" 52 | } 53 | 54 | internal class SelectionSetFieldStepImpl>( 55 | override val parent: SelectionSetStep?, 56 | objectType: T, 57 | field: MetaField<*>? 58 | ) : AbstractSelectionSetFieldsContainerStep(objectType, field), SelectionSetFieldStep { 59 | override val string: String? = field?.name 60 | } 61 | 62 | internal class SelectionSetOutputFieldStepImpl( 63 | override val parent: SelectionSetStep?, 64 | field: MetaField<*>, 65 | ) : AbstractSelectionSetFinalizeStep(), SelectionSetSingleOutputStep { 66 | override val string: String = field.name 67 | } 68 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-util/src/main/kotlin/com/auritylab/graphql/kotlin/toolkit/util/selection/steps/SelectionSetSteps.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.util.selection.steps 2 | 3 | import com.auritylab.graphql.kotlin.toolkit.codegenbinding.types.MetaField 4 | import com.auritylab.graphql.kotlin.toolkit.codegenbinding.types.MetaFieldWithReference 5 | import com.auritylab.graphql.kotlin.toolkit.codegenbinding.types.MetaFieldsContainer 6 | 7 | /** 8 | * Describes a steps for a selection set. The step basically just represents a string. 9 | * A step may have a parent step to represent a chain of steps. A step must be immutable. 10 | * 11 | */ 12 | interface SelectionSetStep { 13 | /** 14 | * The parent step. If this step is the root step, then null will be returned. 15 | */ 16 | val parent: SelectionSetStep? 17 | 18 | /** 19 | * String presentation of this step. 20 | */ 21 | val string: String? 22 | 23 | companion object { 24 | /** 25 | * Will start a new selection set with a [SelectionSetRootStep]. 26 | */ 27 | fun > start(objectType: T): SelectionSetRootStep { 28 | return SelectionSetRootStepImpl(objectType) 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * Describes a step with which other fields can be accessed. 35 | * @param T Type of the ObjectType which which defines the available fields. 36 | */ 37 | interface SelectionSetFieldsContainerStep> : SelectionSetStep { 38 | /** 39 | * Will access the field with reference on type [T]. This will return a [SelectionSetFieldStep] which can be 40 | * used to access fields in its type. 41 | */ 42 | fun > fieldRef(resolver: T.() -> MetaFieldWithReference): SelectionSetFieldStep 43 | 44 | /** 45 | * Will access the field on type [T]. This will return a [SelectionSetOutputStep]. 46 | */ 47 | fun field(resolver: T.() -> MetaField<*>): SelectionSetSingleOutputStep 48 | 49 | /** 50 | * Will return a [SelectionSetWildcardStep]. 51 | */ 52 | fun wildcard(): SelectionSetWildcardStep 53 | } 54 | 55 | /** 56 | * Describes a root step with no parent. The root step has multiple fields. 57 | */ 58 | interface SelectionSetRootStep> : SelectionSetStep, 59 | SelectionSetFieldsContainerStep { 60 | override val parent: SelectionSetStep? 61 | get() = null 62 | override val string: String? 63 | get() = null 64 | } 65 | 66 | /** 67 | * Describes a final step in the builder. This is capable of building the full pattern through [buildPattern]. 68 | */ 69 | interface SelectionSetOutputStep : SelectionSetStep { 70 | /** 71 | * Will build the pattern using all previous steps. 72 | */ 73 | fun buildPattern(): String 74 | } 75 | 76 | /** 77 | * Describes an [SelectionSetOutputStep] which may provide multiple fields. 78 | */ 79 | interface SelectionSetMultiOutputStep : SelectionSetOutputStep 80 | 81 | /** 82 | * Describes an [SelectionSetOutputStep] which may provide a single field. 83 | */ 84 | interface SelectionSetSingleOutputStep : SelectionSetOutputStep 85 | 86 | /** 87 | * Describes a step which represents a wildcard. It may provide multiple fields and therefore implements 88 | * [SelectionSetMultiOutputStep]. 89 | */ 90 | interface SelectionSetWildcardStep : SelectionSetMultiOutputStep, SelectionSetOutputStep 91 | 92 | /** 93 | * Describes step which represents a single field without a reference. It may provide a single field and therefore 94 | * implements [SelectionSetSingleOutputStep]. 95 | */ 96 | interface SelectionSetFieldStep> : SelectionSetSingleOutputStep, 97 | SelectionSetFieldsContainerStep 98 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-util/src/test/kotlin/com/auritylab/graphql/kotlin/toolkit/util/jpa/_TestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.auritylab.graphql.kotlin.toolkit.util.jpa 2 | 3 | import graphql.ExecutionInput 4 | import graphql.GraphQL 5 | import graphql.schema.DataFetcher 6 | import graphql.schema.DataFetchingFieldSelectionSet 7 | import graphql.schema.FieldCoordinates 8 | import graphql.schema.GraphQLCodeRegistry 9 | import graphql.schema.GraphQLSchema 10 | import graphql.schema.idl.RuntimeWiring 11 | import graphql.schema.idl.SchemaGenerator 12 | import graphql.schema.idl.SchemaParser 13 | 14 | /** 15 | * Utils class which holds methods to simplify testing. 16 | */ 17 | object _TestUtils { 18 | /** 19 | * Will load the "schema.graphqls" file from the resources. If the file was not found on the resources, an 20 | * exception will be thrown. 21 | */ 22 | private fun loadSchema(): String = 23 | javaClass.classLoader.getResourceAsStream("schema.graphqls")?.reader()?.readText() 24 | ?: throw IllegalStateException("Schema not found") 25 | 26 | /** 27 | * Will load the "query.graph" file from the resources. If the file was not found on the resources, an exception 28 | * will be thrown. The file can hold multiple operations, therefore we only need to load one file here. 29 | */ 30 | private fun loadQuery(): String = javaClass.classLoader.getResourceAsStream("query.graphql")?.reader()?.readText() 31 | ?: throw IllegalStateException("Query not found") 32 | 33 | /** 34 | * Will create a [GraphQLSchema] based on the schema which will be loaded through [loadSchema]. The wiring for the 35 | * schema will be initialized using the given [dataFetchers]. 36 | */ 37 | fun createSchema(dataFetchers: Map>): GraphQLSchema { 38 | val codeRegistry = GraphQLCodeRegistry.newCodeRegistry() 39 | dataFetchers.forEach { (key, value) -> codeRegistry.dataFetcher(key, value) } 40 | 41 | val wiring = RuntimeWiring.newRuntimeWiring() 42 | .codeRegistry(codeRegistry) 43 | .build() 44 | 45 | return SchemaGenerator().makeExecutableSchema(SchemaParser().parse(loadSchema()), wiring) 46 | } 47 | 48 | /** 49 | * Will create a [GraphQLSchema] based on the schema which will be loaded through [loadSchema]. This will be 50 | * created using an empty runtime wiring. 51 | */ 52 | fun createSchema(): GraphQLSchema { 53 | return createSchema(mapOf()) 54 | } 55 | 56 | fun resolveSelection(): DataFetchingFieldSelectionSet { 57 | var selection: DataFetchingFieldSelectionSet? = null 58 | 59 | val schema = createSchema( 60 | mapOf( 61 | FieldCoordinates.coordinates("Query", "getUsers") to DataFetcher { env -> 62 | selection = env.selectionSet 63 | listOf() 64 | } 65 | ) 66 | ) 67 | val gql = GraphQL.newGraphQL(schema).build() 68 | 69 | gql.execute(ExecutionInput.newExecutionInput().query(loadQuery())) 70 | 71 | return selection!! 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-util/src/test/resources/.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "schemaPath": "./test.graphqls" 4 | } 5 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-util/src/test/resources/query.graphql: -------------------------------------------------------------------------------- 1 | query GetUsersAdvanced { 2 | getUsers { 3 | id 4 | name 5 | surname 6 | emails1: emails { 7 | id 8 | address 9 | countryCode 10 | countries1: countries { 11 | id 12 | countryCode 13 | } 14 | 15 | countries2: countries { 16 | id 17 | countryCode 18 | } 19 | } 20 | 21 | emails2: emails { 22 | countries1: countries { 23 | id 24 | countryCode 25 | } 26 | 27 | countries2: countries { 28 | id 29 | countryCode 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /graphql-kotlin-toolkit-util/src/test/resources/schema.graphqls: -------------------------------------------------------------------------------- 1 | directive @kEntityHint(hints: [String!]!) on FIELD_DEFINITION 2 | 3 | 4 | schema { 5 | query: Query 6 | } 7 | 8 | type Query { 9 | getUsers: [User]! 10 | getUser: User! 11 | } 12 | 13 | type User { 14 | id: ID! 15 | name: String 16 | surname: String 17 | 18 | emails: [UserEmail]! @kEntityHint(hints: ["emails", "defaultEmails"]) 19 | } 20 | 21 | type UserEmail { 22 | id: ID! 23 | address: String! 24 | 25 | countries: [Country]! @kEntityHint(hints: ["countries"]) 26 | countryCode: String! @kEntityHint(hints: ["countries.code"]) 27 | } 28 | 29 | type Country { 30 | id: ID! 31 | countryCode: String 32 | } 33 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | # This simplifies the release process of this project. 2 | # This basically just calls the "allPublish" task but disables parallel executions, because it may cause some strange errors. 3 | 4 | ./gradlew allPublish --no-parallel 5 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "graphql-kotlin-toolkit" 2 | 3 | // Cache build artifacts, so expensive operations do not need to be re-computed. 4 | buildCache { 5 | local { 6 | isEnabled = !(System.getenv().containsKey("CI")) 7 | } 8 | } 9 | 10 | include(":graphql-kotlin-toolkit-codegen") 11 | include(":graphql-kotlin-toolkit-codegen-binding") 12 | include(":graphql-kotlin-toolkit-gradle-plugin") 13 | include(":graphql-kotlin-toolkit-spring-boot") 14 | include(":graphql-kotlin-toolkit-common") 15 | include(":graphql-kotlin-toolkit-util") 16 | --------------------------------------------------------------------------------