├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── CONTRIBUTING.md └── workflows │ ├── main.yml │ ├── release.yml │ └── beta_release.yml ├── logo.png ├── lombok.config ├── src ├── test │ ├── resources │ │ ├── top_level │ │ │ ├── goals-service │ │ │ │ ├── schema.graphqls │ │ │ │ └── queries.graphqls │ │ │ ├── type-extensions │ │ │ │ ├── schema-person-extensions2.graphqls │ │ │ │ ├── schema-teacher-extension.graphqls │ │ │ │ ├── schema.graphqls │ │ │ │ ├── schema-person.graphqls │ │ │ │ ├── schema-actor.graphqls │ │ │ │ ├── schema-teacher.graphqls │ │ │ │ ├── schema-operations.graphqls │ │ │ │ └── schema-person-extensions1.graphqls │ │ │ ├── federation │ │ │ │ ├── employee.graphqls │ │ │ │ ├── review.graphqls │ │ │ │ ├── review-requires.graphqls │ │ │ │ ├── inventory.graphqls │ │ │ │ ├── automakerEnum1.graphqls │ │ │ │ ├── automakerEnum2.graphqls │ │ │ │ └── automakerInvalidInterface.graphqls │ │ │ ├── user-and-pets │ │ │ │ ├── mock-responses │ │ │ │ │ ├── get-petById-1.json │ │ │ │ │ ├── get-petById-2.json │ │ │ │ │ └── get-userById-1.json │ │ │ │ ├── user-schema.graphqls │ │ │ │ ├── user-pet-link.graphqls │ │ │ │ └── schema-pets.graphqls │ │ │ ├── person │ │ │ │ ├── schema1.graphqls │ │ │ │ └── mock-responses │ │ │ │ │ └── get-person-1.json │ │ │ ├── books-and-pets │ │ │ │ ├── schema-books.graphqls │ │ │ │ ├── pet-author-link.graphqls │ │ │ │ ├── mock-responses │ │ │ │ │ ├── get-petById-1to3.json │ │ │ │ │ ├── get-pets.json │ │ │ │ │ ├── get-books-missing-field-link.json │ │ │ │ │ ├── get-books.json │ │ │ │ │ ├── get-petById-1to3-with-error-no-paths.json │ │ │ │ │ └── get-petById-1to3-with-error.json │ │ │ │ └── schema-pets.graphqls │ │ │ ├── user │ │ │ │ ├── user-schema.graphqls │ │ │ │ └── mock-responses │ │ │ │ │ └── get-users.json │ │ │ └── starwars │ │ │ │ └── schema-starwars.graphqls │ │ └── nested │ │ │ ├── chained-types │ │ │ ├── schema-a.graphqls │ │ │ ├── schema-e.graphqls │ │ │ ├── schema-f.graphqls │ │ │ ├── schema-b.graphqls │ │ │ ├── schema-c.graphqls │ │ │ ├── schema-d.graphqls │ │ │ ├── schema-g.graphqls │ │ │ └── schema-h.graphqls │ │ │ ├── turbo │ │ │ └── schema.graphqls │ │ │ ├── v4os │ │ │ └── schema.graphqls │ │ │ └── books-pets-person │ │ │ ├── schema-person.graphqls │ │ │ ├── schema-books.graphqls │ │ │ ├── mock-responses │ │ │ ├── get-books.json │ │ │ ├── get-pets.json │ │ │ └── get-pets-via-field-resolver.json │ │ │ ├── pet-author-link.graphqls │ │ │ └── schema-pets.graphqls │ ├── java │ │ └── com │ │ │ └── intuit │ │ │ └── graphql │ │ │ └── orchestrator │ │ │ ├── testhelpers │ │ │ ├── CustomAssertions.java │ │ │ ├── ServiceProviderMockResponse.java │ │ │ ├── ExecutionInputMatcher.java │ │ │ ├── TestFileLoader.java │ │ │ ├── JsonTestUtils.java │ │ │ └── UnifiedXtextGraphBuilder.java │ │ │ ├── SelectionSetUtil.java │ │ │ ├── ExecutionInputTestUtil.java │ │ │ ├── MutablePetsService.java │ │ │ ├── PersonService.java │ │ │ ├── NestedBooksService.java │ │ │ └── NestedPetsService.java │ └── groovy │ │ ├── com │ │ └── intuit │ │ │ └── graphql │ │ │ └── orchestrator │ │ │ ├── utils │ │ │ ├── DirectiveUtilSpec.groovy │ │ │ ├── QueryPathUtilsSpec.groovy │ │ │ ├── CreateTypeExceptionSpec.groovy │ │ │ ├── CreateGraphQLValueExceptionSpec.groovy │ │ │ ├── DescriptionUtilsSpec.groovy │ │ │ ├── FederationUtilsSpec.groovy │ │ │ ├── FieldReferenceUtilSpec.groovy │ │ │ └── GraphQLUtilSpec.groovy │ │ │ ├── authorization │ │ │ ├── DefaultFieldAuthorizationSpec.groovy │ │ │ └── FieldAuthorizationResultSpec.groovy │ │ │ ├── schema │ │ │ ├── OperationSpec.groovy │ │ │ ├── SchemaParseExceptionSpec.groovy │ │ │ ├── RuntimeGraphSpec.groovy │ │ │ ├── transform │ │ │ │ └── ExplicitTypeResolverSpec.groovy │ │ │ ├── ServiceMetadataImplSpec.groovy │ │ │ └── TypeMetadataSpec.groovy │ │ │ ├── resolverdirective │ │ │ ├── ResolverArgumentPrematureLeafTypeSpec.groovy │ │ │ ├── ResolverArgumentFieldRootObjectDoesNotExistSpec.groovy │ │ │ ├── ResolverArgumentFieldNotInSchemaSpec.groovy │ │ │ ├── ResolverArgumentDirectiveSpec.groovy │ │ │ ├── ResolverArgumentDefinitionSpec.groovy │ │ │ ├── FieldNotFoundInParentExceptionSpec.groovy │ │ │ ├── FieldValueIsNullInParentExceptionSpec.groovy │ │ │ └── ResolverArgumentNotAFieldOfParentExceptionSpec.groovy │ │ │ ├── integration │ │ │ ├── DownstreamGraphqlErrorMappingSpec.groovy │ │ │ ├── DownstreamRestErrorMappingSpec.groovy │ │ │ ├── DownstreamEntityErrorMappingSpec.groovy │ │ │ ├── DirectiveValidationSpec.groovy │ │ │ ├── NestedFieldsArgumentsSpec.groovy │ │ │ ├── NestedFieldsDirectivesSpec.groovy │ │ │ ├── MutationNestedFieldsArgumentsSpec.groovy │ │ │ ├── GraphQLOrchestratorSpec.groovy │ │ │ └── SingleServiceSpec.groovy │ │ │ ├── batch │ │ │ ├── BatchLoaderExecutionHooksSpec.groovy │ │ │ └── QueryResponseModifierSpec.groovy │ │ │ ├── xtext │ │ │ └── XtextScalarsSpec.groovy │ │ │ ├── fieldresolver │ │ │ ├── FieldResolverArgumentExceptionSpec.groovy │ │ │ └── FieldResolverGraphQLErrorSpec.groovy │ │ │ ├── datafetcher │ │ │ ├── XtextResolverArgumentSpec.groovy │ │ │ └── FieldResolverDirectiveDataFetcherSpec.groovy │ │ │ └── federation │ │ │ └── metadata │ │ │ ├── FederationMetadataSpec.groovy │ │ │ └── KeyDirectiveMetadataSpec.groovy │ │ └── helpers │ │ └── SchemaTestUtil.groovy └── main │ ├── resources │ ├── authzpolicy_directive_definition.graphqls │ └── federation_built_in_directives.graphqls │ └── java │ └── com │ └── intuit │ └── graphql │ └── orchestrator │ ├── schema │ ├── transform │ │ ├── Transformer.java │ │ ├── FieldMergeException.java │ │ ├── ExplicitTypeResolver.java │ │ ├── ResolverArgumentListTypeNotSupported.java │ │ ├── DirectivesTransformer.java │ │ ├── DomainTypesTransformer.java │ │ └── UnionAndInterfaceTransformer.java │ ├── fold │ │ └── Foldable.java │ ├── SchemaTransformationException.java │ ├── GraphQLObjects.java │ ├── type │ │ └── conflict │ │ │ └── resolver │ │ │ ├── TypeConflictException.java │ │ │ └── TypeConflictResolver.java │ ├── SchemaParseException.java │ ├── TypeMetadata.java │ ├── ServiceMetadata.java │ └── Operation.java │ ├── utils │ ├── CreateTypeException.java │ ├── CreateGraphQLValueException.java │ ├── XtextSerializer.java │ ├── ExtendedScalarsSupport.java │ ├── IntrospectionUtil.java │ ├── RenameDirectiveUtil.java │ ├── XtextGraphUtils.java │ ├── ExecutionPathUtils.java │ ├── QueryPathUtils.java │ ├── FieldReferenceUtil.java │ ├── DescriptionUtils.java │ ├── TypeReferenceUtil.java │ ├── SelectionSetUtil.java │ └── QueryDirectivesUtil.java │ ├── batch │ ├── QueryResponseModifier.java │ ├── QueryExecutor.java │ ├── BatchResultTransformer.java │ ├── DataLoaderKeyUtil.java │ ├── DefaultBatchResultTransformer.java │ ├── DefaultQueryResponseModifier.java │ └── VariableReferenceExtractor.java │ ├── resolverdirective │ ├── ResolverDirectiveException.java │ ├── UnexpectedResolverDirectiveParentType.java │ ├── NotAValidFieldReference.java │ ├── ArgumentDefinitionNotAllowed.java │ ├── MultipleResolverDirectiveDefinition.java │ ├── NotAValidLocationForFieldResolverDirective.java │ ├── ExternalTypeNotfoundException.java │ ├── ResolverArgumentNotAFieldOfParentException.java │ ├── ResolverArgumentFieldRootObjectDoesNotExist.java │ ├── ResolverArgumentPrematureLeafType.java │ ├── ResolverArgumentFieldNotInSchema.java │ ├── ResolverArgumentDefinition.java │ ├── ResolverArgumentTypeMismatch.java │ └── ResolverArgumentLeafTypeNotSame.java │ ├── stitching │ ├── StitchingException.java │ ├── InvalidDirectivePairingException.java │ └── Stitcher.java │ ├── authorization │ ├── SelectionSetMetadata.java │ ├── DownstreamQueryRedactorResult.java │ ├── DefaultFieldAuthorization.java │ ├── FieldAuthorizationEnvironment.java │ ├── FieldAuthorizationResult.java │ ├── ValidateMultipleDirectivesCoexist.java │ └── FieldAuthorization.java │ ├── exceptions │ └── InvalidRenameException.java │ ├── datafetcher │ ├── ServiceAwareDataFetcher.java │ ├── AliasablePropertyDataFetcher.java │ ├── ServiceDataFetcher.java │ ├── ArgumentAppenderVisitor.java │ └── FieldResolverDirectiveDataFetcher.java │ ├── federation │ ├── exceptions │ │ ├── DirectiveMissingRequiredArgumentException.java │ │ ├── SharedOwnershipException.java │ │ ├── EmptyFieldsArgumentFederationDirective.java │ │ ├── ExternalFieldNotFoundInBaseException.java │ │ ├── InvalidFieldSetReferenceException.java │ │ ├── BaseTypeNotFoundException.java │ │ └── IncorrectDirectiveArgumentSizeException.java │ ├── validators │ │ └── ExternalValidator.java │ ├── FieldSetUtils.java │ ├── metadata │ │ └── KeyDirectiveMetadata.java │ ├── Federation2PureGraphQLUtil.java │ └── EntityDataFetcher.java │ ├── fieldresolver │ ├── QueryOperationFactory.java │ ├── TypenameInjector.java │ ├── FieldResolverValidator.java │ ├── FieldResolverException.java │ ├── FieldResolverArgumentException.java │ └── ValueTemplate.java │ ├── common │ └── ArgumentValueResolver.java │ ├── metadata │ └── RenamedMetadata.java │ ├── xtext │ ├── FieldContext.java │ └── XtextGraphBuilder.java │ ├── ServiceProvider.java │ └── VirtualOrchestratorProvider.java ├── codecov.yaml ├── CHANGELOG.md ├── .gitignore └── CODE_OF_CONDUCT.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @ashpak-shaikh 2 | @CNAChino 3 | @scanbns -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graph-quilt/graphql-orchestrator-java/HEAD/logo.png -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | config.stopBubbling = true 2 | lombok.addLombokGeneratedAnnotation = true 3 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # What Changed 2 | 3 | # Why 4 | 5 | Todo: 6 | 7 | - [ ] Add tests 8 | - [ ] Add docs -------------------------------------------------------------------------------- /src/test/resources/top_level/goals-service/schema.graphqls: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | } 4 | 5 | type Query { 6 | 7 | } -------------------------------------------------------------------------------- /src/test/resources/nested/chained-types/schema-a.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | a : A 3 | } 4 | 5 | type A { 6 | a1 : ID! 7 | a2 : String 8 | } -------------------------------------------------------------------------------- /src/test/resources/top_level/type-extensions/schema-person-extensions2.graphqls: -------------------------------------------------------------------------------- 1 | extend union EmployedPersonsUnion = Actor 2 | 3 | extend enum HobbyEnum { 4 | BIKING 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/authzpolicy_directive_definition.graphqls: -------------------------------------------------------------------------------- 1 | directive @authzPolicy(id: String, ruleInputs:[RuleInput]!) on FIELD_DEFINITION 2 | input RuleInput { key: String! value: [String]!} 3 | -------------------------------------------------------------------------------- /src/test/resources/nested/chained-types/schema-e.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | a : A 3 | } 4 | 5 | type A { 6 | e : E 7 | } 8 | 9 | type E { 10 | ea : ID! 11 | eb : String 12 | } -------------------------------------------------------------------------------- /src/test/resources/top_level/type-extensions/schema-teacher-extension.graphqls: -------------------------------------------------------------------------------- 1 | extend type Teacher { 2 | subjects : [String] 3 | } 4 | 5 | extend input InputTeacher { 6 | subjects : [String] 7 | } -------------------------------------------------------------------------------- /src/test/resources/top_level/type-extensions/schema.graphqls: -------------------------------------------------------------------------------- 1 | schema { 2 | query: QueryType 3 | mutation: MutationType 4 | } 5 | 6 | type QueryType { 7 | } 8 | 9 | type MutationType { 10 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/schema/transform/Transformer.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema.transform; 2 | 3 | public interface Transformer { 4 | 5 | T transform(S source); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/test/resources/top_level/federation/employee.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | employeeById(id: ID!): Employee 3 | } 4 | 5 | type Employee @key(fields: "id"){ 6 | id: ID! 7 | username: String! 8 | password: String! 9 | } -------------------------------------------------------------------------------- /src/test/resources/nested/chained-types/schema-f.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | a : A 3 | } 4 | 5 | type A { 6 | e : E 7 | } 8 | 9 | type E { 10 | f : F 11 | } 12 | 13 | type F { 14 | f1 : ID! 15 | f2 : String 16 | } -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.codecov.io/docs/codecov-yaml 2 | codecov: 3 | branch: master 4 | coverage: 5 | status: 6 | patch: 7 | default: 8 | target: 50% 9 | 10 | comment: 11 | layout: "diff, flags, files:10, footer" -------------------------------------------------------------------------------- /src/test/resources/nested/chained-types/schema-b.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | a : A 3 | } 4 | 5 | type A { 6 | b : B 7 | } 8 | 9 | type B { 10 | b1 : ID! 11 | b2 : String 12 | z : BZ 13 | } 14 | 15 | type BZ { 16 | zb : String 17 | } -------------------------------------------------------------------------------- /src/test/resources/nested/chained-types/schema-c.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | a : A 3 | } 4 | 5 | type A { 6 | c : C 7 | } 8 | 9 | type C { 10 | c1 : ID! 11 | c2 : String 12 | z : CZ 13 | } 14 | 15 | type CZ { 16 | zc : String 17 | } -------------------------------------------------------------------------------- /src/test/resources/nested/chained-types/schema-d.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | a : A 3 | } 4 | 5 | type A { 6 | d : D 7 | } 8 | 9 | type D { 10 | d1 : ID! 11 | d2 : String 12 | z : DZ 13 | } 14 | 15 | type DZ { 16 | zd : String 17 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/utils/CreateTypeException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils; 2 | 3 | class CreateTypeException extends RuntimeException { 4 | 5 | CreateTypeException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/schema/fold/Foldable.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema.fold; 2 | 3 | import java.util.Collection; 4 | 5 | public interface Foldable { 6 | 7 | U fold(U initVal, Collection list); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/test/resources/top_level/goals-service/queries.graphqls: -------------------------------------------------------------------------------- 1 | # The Root Query for the application 2 | extend type Query { 3 | # Get User Goals 4 | userGoals : [UserGoal!]! 5 | # Get Images associated to a User Goal 6 | userGoalImages(userGoalId: Long): [Image!] 7 | } -------------------------------------------------------------------------------- /src/test/resources/top_level/user-and-pets/mock-responses/get-petById-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "petById1_0": { 4 | "__typename" : "Dog", 5 | "id": "pet-1", 6 | "name": "Lassie", 7 | "dogBreed": "COLLIE" 8 | } 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/test/resources/nested/chained-types/schema-g.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | a : A 3 | } 4 | 5 | type A { 6 | e : E 7 | } 8 | 9 | type E { 10 | f : F 11 | } 12 | 13 | type F { 14 | g : G 15 | } 16 | 17 | type G { 18 | g1 : ID! 19 | g2 : String 20 | } -------------------------------------------------------------------------------- /src/test/resources/top_level/user-and-pets/user-schema.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | userById(id: ID!): User 3 | } 4 | 5 | type User { 6 | id : ID! 7 | firstName: String! 8 | lastName: String! 9 | email: String 10 | petId1: String 11 | petId2: String 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/utils/CreateGraphQLValueException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils; 2 | 3 | class CreateGraphQLValueException extends RuntimeException { 4 | 5 | CreateGraphQLValueException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/test/resources/top_level/type-extensions/schema-person.graphqls: -------------------------------------------------------------------------------- 1 | 2 | interface PersonInterface { 3 | id: ID 4 | name: String 5 | phoneNumber : PhoneNumber 6 | hobby : HobbyEnum 7 | } 8 | 9 | enum HobbyEnum { 10 | PHOTOGRAPHY, 11 | HIKING 12 | } 13 | 14 | scalar PhoneNumber -------------------------------------------------------------------------------- /src/test/resources/top_level/user-and-pets/mock-responses/get-petById-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "petById2_0": { 4 | "__typename" : "Cat", 5 | "id": "pet-2", 6 | "name": "Garfield", 7 | "milkPerDay": 3, 8 | "catBreed": "PERSIAN" 9 | } 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/test/resources/nested/chained-types/schema-h.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | a : A 3 | } 4 | 5 | type A { 6 | e : E 7 | } 8 | 9 | type E { 10 | f : F 11 | } 12 | 13 | type F { 14 | g : G 15 | } 16 | 17 | type G { 18 | h : H 19 | } 20 | type H { 21 | h1 : ID! 22 | h2 : String 23 | } -------------------------------------------------------------------------------- /src/test/resources/nested/turbo/schema.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | consumer: ConsumerType 3 | } 4 | 5 | type ConsumerType { 6 | finance: FinanceType 7 | turboExperiences: ExperienceType 8 | } 9 | 10 | type FinanceType { 11 | fieldTurbo: Int 12 | } 13 | 14 | type ExperienceType { 15 | experienceField: String 16 | } -------------------------------------------------------------------------------- /src/test/resources/nested/v4os/schema.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | consumer: ConsumerType 3 | } 4 | 5 | type ConsumerType { 6 | finance: FinanceType 7 | financialProfile: FinancialProfileType 8 | } 9 | 10 | type FinanceType { 11 | fieldFinance: Int 12 | } 13 | 14 | type FinancialProfileType { 15 | fieldFinancialProfile: String 16 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ### Added 9 | - Initial release. 10 | -------------------------------------------------------------------------------- /src/test/resources/nested/books-pets-person/schema-person.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | person : Person 3 | } 4 | 5 | type Person { 6 | id: ID 7 | name: String 8 | address: Address 9 | } 10 | 11 | type Address { 12 | id: ID 13 | street: String 14 | city: String 15 | zip: String 16 | state: String 17 | country: String 18 | } 19 | -------------------------------------------------------------------------------- /src/test/resources/top_level/user-and-pets/mock-responses/get-userById-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "userById": { 4 | "id": "user-1", 5 | "firstName": "Delilah", 6 | "lastName": "Hadfield", 7 | "email": "delilah.hadfield@mail.com", 8 | "petId1": "pet-1", 9 | "petId2": "pet-2" 10 | } 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/test/java/com/intuit/graphql/orchestrator/testhelpers/CustomAssertions.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.testhelpers; 2 | 3 | import graphql.ExecutionResult; 4 | 5 | public class CustomAssertions { 6 | 7 | public static ExecutionResultAssert assertThat(ExecutionResult actual) { 8 | return new ExecutionResultAssert(actual); 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /src/test/resources/nested/books-pets-person/schema-books.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | person : Person 3 | } 4 | 5 | type Person { 6 | book : Book 7 | } 8 | 9 | type Book { 10 | id: ID 11 | name: String 12 | pageCount: Int 13 | author: Author 14 | } 15 | 16 | type Author { 17 | id: ID 18 | firstName: String 19 | lastName: String 20 | } 21 | -------------------------------------------------------------------------------- /src/test/resources/top_level/federation/review.graphqls: -------------------------------------------------------------------------------- 1 | extend type Employee @key(fields: "id"){ 2 | id: ID! @external 3 | review: Review 4 | } 5 | 6 | type Store @extends @key(fields: "id") { 7 | id: ID @external 8 | review: Review 9 | } 10 | 11 | type Review { 12 | reviewId: ID! 13 | rating: Int! 14 | comments: String 15 | test: String 16 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/batch/QueryResponseModifier.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.batch; 2 | 3 | import graphql.execution.DataFetcherResult; 4 | import java.util.Map; 5 | 6 | @FunctionalInterface 7 | public interface QueryResponseModifier { 8 | 9 | DataFetcherResult> modify(Map queryResponse); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/utils/DirectiveUtilSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils 2 | 3 | import spock.lang.Specification 4 | 5 | class DirectiveUtilSpec extends Specification { 6 | 7 | def "build Deprecation Reason Null Input Test"() { 8 | expect: 9 | DirectivesUtil.buildDeprecationReason(null) == null 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/test/resources/nested/books-pets-person/mock-responses/get-books.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "person": { 4 | "book": { 5 | "pageCount": "2000", 6 | "author": { 7 | "id": "author-1", 8 | "lastName": "AuthorOne" 9 | }, 10 | "name": "GraphQL Advanced Stitching", 11 | "id": "book-1" 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/schema/SchemaTransformationException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema; 2 | 3 | import com.intuit.graphql.orchestrator.stitching.StitchingException; 4 | 5 | public class SchemaTransformationException extends StitchingException { 6 | 7 | public SchemaTransformationException(String message) { 8 | super(message); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/resolverdirective/ResolverDirectiveException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective; 2 | 3 | import com.intuit.graphql.orchestrator.stitching.StitchingException; 4 | 5 | public class ResolverDirectiveException extends StitchingException { 6 | 7 | public ResolverDirectiveException(String message) { 8 | super(message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | 14 | /target 15 | /.settings 16 | /.classpath 17 | /.project 18 | /velocity.log 19 | *.bak 20 | .idea/* 21 | bin/* 22 | web/* 23 | *.iml 24 | *.DS_Store 25 | output* 26 | -------------------------------------------------------------------------------- /src/test/resources/top_level/federation/review-requires.graphqls: -------------------------------------------------------------------------------- 1 | extend type Employee @key(fields: "id"){ 2 | id: ID! @external 3 | review: Review 4 | } 5 | 6 | type Store @extends @key(fields: "id") { 7 | id: ID @external 8 | name: String @external 9 | review: Review @requires(fields: "name") 10 | } 11 | 12 | type Review { 13 | reviewId: ID! 14 | rating: Int! 15 | comments: String 16 | } -------------------------------------------------------------------------------- /src/test/resources/top_level/person/schema1.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | person : Person 3 | personById(id: ID) : Person 4 | } 5 | 6 | type Person { 7 | id: ID 8 | name: String 9 | address: Address 10 | bookId: ID 11 | income: Int 12 | debt: Int 13 | } 14 | 15 | type Address { 16 | id: ID 17 | street: String 18 | city: String 19 | zip: String 20 | state: String 21 | country: String 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/schema/GraphQLObjects.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema; 2 | 3 | /** 4 | * Helper method that suppress unsafe casts for GraphQL Objects. 5 | */ 6 | public class GraphQLObjects { 7 | 8 | private GraphQLObjects() { 9 | 10 | } 11 | 12 | @SuppressWarnings("unchecked") 13 | public static T cast(Object o) { 14 | return (T) o; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/schema/type/conflict/resolver/TypeConflictException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema.type.conflict.resolver; 2 | 3 | import com.intuit.graphql.orchestrator.stitching.StitchingException; 4 | 5 | public class TypeConflictException extends StitchingException { 6 | 7 | public TypeConflictException(String message) { 8 | super(message); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/utils/XtextSerializer.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils; 2 | 3 | import org.eclipse.emf.ecore.EObject; 4 | import org.eclipse.xtext.resource.XtextResource; 5 | 6 | public class XtextSerializer { 7 | 8 | public static String serialize(EObject eObject) { 9 | return ((XtextResource) eObject.eResource()).getSerializer().serialize(eObject); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/test/resources/top_level/person/mock-responses/get-person-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "person": { 4 | "id": "person-1", 5 | "name": "Kevin Whitney", 6 | "address": { 7 | "zip": "12345", 8 | "country": "United States", 9 | "city": "San Diego", 10 | "street": "Lombok Street", 11 | "id": "address-1", 12 | "state": "CA" 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/test/resources/top_level/books-and-pets/schema-books.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | bookById(id: ID): Book 3 | books : [Book] 4 | } 5 | 6 | type Book { 7 | id: ID 8 | name: String 9 | pageCount: Int 10 | weight: Float 11 | isFamilyFriendly: Boolean 12 | author: Author 13 | } 14 | 15 | type Author { 16 | id: ID 17 | firstName: String 18 | lastName: String 19 | petId: String 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/resolverdirective/UnexpectedResolverDirectiveParentType.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective; 2 | 3 | import com.intuit.graphql.orchestrator.stitching.StitchingException; 4 | 5 | public class UnexpectedResolverDirectiveParentType extends StitchingException { 6 | 7 | public UnexpectedResolverDirectiveParentType(String message) { 8 | super(message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/com/intuit/graphql/orchestrator/testhelpers/ServiceProviderMockResponse.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.testhelpers; 2 | 3 | import graphql.ExecutionInput; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | 7 | @Builder 8 | @Getter 9 | public class ServiceProviderMockResponse { 10 | private String expectResponse; 11 | private String expectResponseRaw; 12 | private ExecutionInput forExecutionInput; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/batch/QueryExecutor.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.batch; 2 | 3 | import graphql.ExecutionInput; 4 | import graphql.GraphQLContext; 5 | import java.util.Map; 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | @FunctionalInterface 9 | public interface QueryExecutor { 10 | 11 | CompletableFuture> query(ExecutionInput executionInput, GraphQLContext context); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/schema/transform/FieldMergeException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema.transform; 2 | 3 | import com.intuit.graphql.orchestrator.stitching.StitchingException; 4 | 5 | /** 6 | * A class thrown if an error occured during nested merging. 7 | */ 8 | public class FieldMergeException extends StitchingException { 9 | 10 | public FieldMergeException(String message) { 11 | super(message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/resources/top_level/federation/inventory.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | getSoldProducts: [Product] 3 | getStoreByIdAndName(id: ID!, name: String!): Store 4 | } 5 | 6 | type Store @key(fields: "id") { 7 | id: ID 8 | name: String 9 | sold: [Product] 10 | stock: [Product] 11 | } 12 | 13 | type Employee @key(fields: "id") @extends { 14 | id: ID @external 15 | favoriteItem: Product 16 | } 17 | 18 | type Product { 19 | name: String 20 | } -------------------------------------------------------------------------------- /src/test/resources/top_level/type-extensions/schema-actor.graphqls: -------------------------------------------------------------------------------- 1 | type Actor implements PersonInterface { 2 | id: ID 3 | name: String 4 | address: AddressBase 5 | phoneNumber : PhoneNumber 6 | hobby : HobbyEnum 7 | 8 | movies : [String] 9 | } 10 | 11 | input InputActor{ 12 | id: ID 13 | name: String 14 | address: InputAddressBase 15 | phoneNumber : PhoneNumber 16 | hobby : HobbyEnum 17 | 18 | movies : [String] 19 | } 20 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/utils/QueryPathUtilsSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils 2 | 3 | import spock.lang.Specification 4 | 5 | class QueryPathUtilsSpec extends Specification { 6 | 7 | def "can convert a list of fields to FQN format"() { 8 | when: 9 | String pathString = QueryPathUtils.pathListToFQN(Arrays.asList("a", "b", "c")); 10 | 11 | then: 12 | pathString == "a.b.c" 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/resources/top_level/type-extensions/schema-teacher.graphqls: -------------------------------------------------------------------------------- 1 | type Teacher implements PersonInterface { 2 | id: ID 3 | name: String 4 | address: AddressBase 5 | phoneNumber : PhoneNumber 6 | hobby : HobbyEnum 7 | 8 | school : String 9 | } 10 | 11 | input InputTeacher{ 12 | id: ID 13 | name: String 14 | address: InputAddressBase 15 | phoneNumber : PhoneNumber 16 | hobby : HobbyEnum 17 | 18 | school : String 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/test/resources/top_level/type-extensions/schema-operations.graphqls: -------------------------------------------------------------------------------- 1 | # The Root Query for the application 2 | 3 | extend type QueryType { 4 | # Get User Goals 5 | person : PersonInterface 6 | firstEmployedPerson: EmployedPersonsUnion 7 | secondEmployedPerson: EmployedPersonsUnion 8 | } 9 | 10 | extend type MutationType { 11 | # Get User Goals 12 | addTeacher(teacher : InputTeacher): Teacher 13 | addActor(newActor : InputActor): Actor 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/stitching/StitchingException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.stitching; 2 | 3 | /** 4 | * The base exception class for any error that occured in this library. 5 | */ 6 | public class StitchingException extends RuntimeException { 7 | 8 | public StitchingException(String message, Throwable t) { 9 | super(message, t); 10 | } 11 | 12 | public StitchingException(final String message) { 13 | super(message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/resources/top_level/federation/automakerEnum1.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | getManufacturers: [Manufacturer] 3 | getCars: [Car] 4 | } 5 | 6 | interface Vehicle { 7 | wheels: Int 8 | type: String 9 | } 10 | 11 | type Car implements Vehicle { 12 | wheels: Int 13 | type: String 14 | features: String 15 | make: Manufacturer 16 | model: String 17 | color: String 18 | } 19 | 20 | enum Manufacturer { 21 | TOYOTA 22 | HONDA 23 | CHEVROLET 24 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/authorization/SelectionSetMetadata.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.authorization; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @AllArgsConstructor 7 | public class SelectionSetMetadata { 8 | 9 | @Getter private int remainingSelectionsCount; 10 | @Getter private final String selectionSetPath; 11 | 12 | public void decreaseRemainingSelection() { 13 | --this.remainingSelectionsCount; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/resolverdirective/NotAValidFieldReference.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective; 2 | 3 | public class NotAValidFieldReference extends ResolverDirectiveException { 4 | 5 | private static final String ERR_INVALID_FIELD_REF_FORMAT = "'%s' is not a valid field reference."; 6 | 7 | public NotAValidFieldReference(String resolverArgValue) { 8 | super(String.format(ERR_INVALID_FIELD_REF_FORMAT, resolverArgValue)); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/test/resources/top_level/books-and-pets/pet-author-link.graphqls: -------------------------------------------------------------------------------- 1 | extend type Author { 2 | pet : Pet @resolver(field: "pet" arguments: [{name : "id", value: "$petId"}]) 3 | } 4 | 5 | type Pet {} 6 | 7 | 8 | # ================================ 9 | # define this as built-in directive 10 | directive @resolver(field: String!, arguments: [ResolverArgument!]) on FIELD_DEFINITION 11 | 12 | # define this as built-in type 13 | input ResolverArgument { 14 | name : String! 15 | value : String! 16 | } 17 | -------------------------------------------------------------------------------- /src/test/resources/top_level/federation/automakerEnum2.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | getMakers: [Manufacturer] 3 | getAutos: [Car] 4 | } 5 | 6 | interface Vehicle { 7 | wheels: Int 8 | type: String 9 | features: String 10 | } 11 | 12 | type Car implements Vehicle { 13 | wheels: Int 14 | type: String 15 | features: String 16 | color: String 17 | make: Manufacturer 18 | model: String 19 | } 20 | 21 | 22 | enum Manufacturer { 23 | FORD 24 | HONDA 25 | VOLVO 26 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/exceptions/InvalidRenameException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.exceptions; 2 | 3 | import com.intuit.graphql.orchestrator.stitching.StitchingException; 4 | 5 | public class InvalidRenameException extends StitchingException { 6 | 7 | public InvalidRenameException(String message, Throwable t) { 8 | super(message, t); 9 | } 10 | 11 | public InvalidRenameException(final String message) { 12 | super(message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/batch/BatchResultTransformer.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.batch; 2 | 3 | import graphql.execution.DataFetcherResult; 4 | import graphql.schema.DataFetchingEnvironment; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | @FunctionalInterface 9 | public interface BatchResultTransformer { 10 | 11 | List> toBatchResult(DataFetcherResult> result, 12 | List keys); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/datafetcher/ServiceAwareDataFetcher.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.datafetcher; 2 | 3 | import com.intuit.graphql.orchestrator.ServiceProvider.ServiceType; 4 | import com.intuit.graphql.orchestrator.xtext.DataFetcherContext.DataFetcherType; 5 | import graphql.schema.DataFetcher; 6 | 7 | public interface ServiceAwareDataFetcher extends DataFetcher { 8 | String getNamespace(); 9 | DataFetcherType getDataFetcherType(); 10 | ServiceType getServiceType(); 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | 11 | 12 | 13 | **To Reproduce** 14 | 15 | 16 | 17 | **Expected behavior** 18 | 19 | 20 | 21 | **Additional context** 22 | 23 | -------------------------------------------------------------------------------- /src/main/resources/federation_built_in_directives.graphqls: -------------------------------------------------------------------------------- 1 | scalar _FieldSet 2 | 3 | directive @key(fields: _FieldSet) repeatable on OBJECT | INTERFACE 4 | directive @extends on OBJECT | INTERFACE 5 | directive @external on FIELD_DEFINITION 6 | directive @provides(fields: String!) on FIELD_DEFINITION 7 | directive @requires(fields: _FieldSet!) on FIELD_DEFINITION 8 | 9 | directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION 10 | -------------------------------------------------------------------------------- /src/test/resources/nested/books-pets-person/pet-author-link.graphqls: -------------------------------------------------------------------------------- 1 | extend type Author { 2 | pets : [Pet] @resolver(field: "person.pets" arguments: [{name : "animalType", value: "DOG"} {name : "pureBred", value: "true"}]) 3 | } 4 | 5 | type Pet {} 6 | 7 | 8 | # ================================ 9 | # define this as built-in directive 10 | directive @resolver(field: String!, arguments: [ResolverArgument!]) on FIELD_DEFINITION 11 | 12 | # define this as built-in type 13 | input ResolverArgument { 14 | name : String! 15 | value : String! 16 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/resolverdirective/ArgumentDefinitionNotAllowed.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective; 2 | 3 | public class ArgumentDefinitionNotAllowed extends ResolverDirectiveException { 4 | 5 | private static final String ERR_MSG = "Field %s in container type %s with resolver directive not allowed " 6 | + "to have argument definitions."; 7 | 8 | public ArgumentDefinitionNotAllowed(String fieldName, String containerName) { 9 | super(String.format(ERR_MSG, fieldName, containerName)); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/test/resources/top_level/federation/automakerInvalidInterface.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | getMakers: [Manufacturer] 3 | getAutos: [Car] 4 | } 5 | 6 | interface Vehicle { 7 | wheels: Int 8 | type: String 9 | features: String 10 | newField: String 11 | } 12 | 13 | type Car implements Vehicle { 14 | wheels: Int 15 | type: String 16 | features: String 17 | make: Manufacturer 18 | model: String 19 | color: String 20 | newField: String 21 | } 22 | 23 | 24 | enum Manufacturer { 25 | FORD 26 | HONDA 27 | VOLVO 28 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/stitching/InvalidDirectivePairingException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.stitching; 2 | 3 | 4 | import java.util.List; 5 | 6 | public class InvalidDirectivePairingException extends StitchingException { 7 | 8 | private static final String ERR_MSG = "Field %s in container type %s with resolver directive not allowed " 9 | + "to have argument definitions."; 10 | 11 | public InvalidDirectivePairingException(List directiveNames) { 12 | super(String.format(ERR_MSG, directiveNames.get(0), directiveNames.get(1))); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/authorization/DefaultFieldAuthorizationSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.authorization 2 | 3 | import spock.lang.Specification 4 | 5 | import java.util.concurrent.CompletableFuture 6 | 7 | class DefaultFieldAuthorizationSpec extends Specification { 8 | 9 | def specUnderTest = new DefaultFieldAuthorization() 10 | 11 | def "future auth data must be null"() { 12 | when: 13 | CompletableFuture actual = specUnderTest.getFutureAuthData() 14 | 15 | then: 16 | actual.get() == "" 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/utils/CreateTypeExceptionSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils 2 | 3 | import spock.lang.Specification 4 | 5 | class CreateTypeExceptionSpec extends Specification { 6 | 7 | def "CreateTypeException is instance of RunTimeException and sets the correct error message"() { 8 | given: 9 | CreateTypeException createTypeException = new CreateTypeException("test Message") 10 | expect: 11 | createTypeException instanceof RuntimeException 12 | createTypeException.message == "test Message" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/stitching/Stitcher.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.stitching; 2 | 3 | import com.intuit.graphql.orchestrator.ServiceProvider; 4 | import com.intuit.graphql.orchestrator.schema.RuntimeGraph; 5 | import java.util.List; 6 | 7 | /** 8 | * The interface Stitcher. 9 | */ 10 | public interface Stitcher { 11 | 12 | /** 13 | * Stitch the service context into a runtime graph. 14 | * 15 | * @param serviceProviders the service contexts 16 | * @return the runtime graph 17 | */ 18 | RuntimeGraph stitch(List serviceProviders); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/federation/exceptions/DirectiveMissingRequiredArgumentException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.federation.exceptions; 2 | 3 | import com.intuit.graphql.orchestrator.stitching.StitchingException; 4 | 5 | public class DirectiveMissingRequiredArgumentException extends StitchingException { 6 | private static final String ERROR_MSG = "%s Directive for '%s' cannot have multiple arguments"; 7 | 8 | public DirectiveMissingRequiredArgumentException(String directiveName, String entity) { 9 | super(String.format(ERROR_MSG, directiveName ,entity)); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/federation/exceptions/SharedOwnershipException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.federation.exceptions; 2 | 3 | import com.intuit.graphql.orchestrator.stitching.StitchingException; 4 | 5 | import static java.lang.String.format; 6 | 7 | public class SharedOwnershipException extends StitchingException { 8 | private static final String ERROR_MSG = "No @External directive found. Field '%s' is already declared and defined in the base type."; 9 | 10 | public SharedOwnershipException(String fieldName) { 11 | super(format(ERROR_MSG, fieldName)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/resolverdirective/MultipleResolverDirectiveDefinition.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective; 2 | 3 | import com.intuit.graphql.orchestrator.stitching.StitchingException; 4 | 5 | public class MultipleResolverDirectiveDefinition extends StitchingException { 6 | 7 | private static final String ERROR_MESSAGE = "Expecting to have 1 resolver directive but " 8 | + "found multiple definitions. directives count = %s"; 9 | 10 | public MultipleResolverDirectiveDefinition(int size) { 11 | super(String.format(ERROR_MESSAGE, size)); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/top_level/user-and-pets/user-pet-link.graphqls: -------------------------------------------------------------------------------- 1 | extend type User { 2 | pet1: CatOrDog @resolver(field: "petById1" arguments: [{name : "id", value: "$petId1"}]) 3 | pet2: Pet @resolver(field: "petById2" arguments: [{name : "id", value: "$petId2"}]) 4 | } 5 | 6 | interface Pet 7 | 8 | union CatOrDog 9 | 10 | 11 | # ================================ 12 | # define this as built-in directive 13 | directive @resolver(field: String!, arguments: [ResolverArgument!]) on FIELD_DEFINITION 14 | 15 | # define this as built-in type 16 | input ResolverArgument { 17 | name : String! 18 | value : String! 19 | } 20 | -------------------------------------------------------------------------------- /src/test/resources/top_level/type-extensions/schema-person-extensions1.graphqls: -------------------------------------------------------------------------------- 1 | union EmployedPersonsUnion = Teacher 2 | 3 | extend interface PersonInterface { 4 | address: AddressBase 5 | } 6 | 7 | type AddressBase { 8 | id: ID 9 | street: String 10 | city: String 11 | zip: String 12 | state: String 13 | country: String 14 | } 15 | 16 | input InputAddressBase { 17 | id: ID 18 | street: String 19 | city: String 20 | zip: String 21 | state: String 22 | country: String 23 | } 24 | 25 | #directive @E164 on FIELD_DEFINITION | SCALAR 26 | # 27 | #extend scalar PhoneNumber @E164 28 | 29 | -------------------------------------------------------------------------------- /src/test/resources/top_level/user-and-pets/schema-pets.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | petById1(id: ID!): CatOrDog 3 | petById2(id: ID!): Pet 4 | } 5 | 6 | interface Pet { 7 | id: ID! 8 | name: String! 9 | } 10 | 11 | type Dog implements Pet { 12 | id: ID! 13 | name: String! 14 | dogBreed: DogBreed 15 | } 16 | 17 | enum DogBreed { 18 | BEAGLE, 19 | COLLIE 20 | } 21 | 22 | type Cat implements Pet { 23 | id: ID! 24 | name: String! 25 | milkPerDay: Int 26 | catBreed: CatBreed 27 | } 28 | 29 | enum CatBreed { 30 | PERSIAN, 31 | SIAMESE 32 | } 33 | 34 | 35 | union CatOrDog = Dog | Cat -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/federation/exceptions/EmptyFieldsArgumentFederationDirective.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.federation.exceptions; 2 | 3 | import com.intuit.graphql.orchestrator.stitching.StitchingException; 4 | 5 | public class EmptyFieldsArgumentFederationDirective extends StitchingException { 6 | private static final String ERROR_MSG = "Fields argument cannot be empty for %s directive in '%s'"; 7 | 8 | public EmptyFieldsArgumentFederationDirective(String containerName, String directiveName) { 9 | super(String.format(ERROR_MSG, directiveName, containerName)); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/resolverdirective/NotAValidLocationForFieldResolverDirective.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective; 2 | 3 | public class NotAValidLocationForFieldResolverDirective extends ResolverDirectiveException { 4 | 5 | private static final String ERR_MSG = "Field %s with resolver directive is defined in " 6 | + "container type %s that is not an Object Type or Object Type extension"; 7 | 8 | public NotAValidLocationForFieldResolverDirective(String fieldName, String containerName) { 9 | super(String.format(ERR_MSG, fieldName, containerName)); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/test/resources/nested/books-pets-person/schema-pets.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | person : Person 3 | } 4 | 5 | type Person { 6 | pets(animalType: AnimalType!, pureBred: Boolean!): [Pet] 7 | } 8 | 9 | type Mutation { 10 | addPet(pet: InputPet!): Pet 11 | } 12 | 13 | type Pet{ 14 | id: ID! 15 | name: String! 16 | age: Int 17 | weight: Float 18 | purebred: Boolean 19 | tag: String 20 | } 21 | 22 | input InputPet{ 23 | id: ID! 24 | name: String! 25 | age: Int 26 | weight: Float 27 | purebred: Boolean 28 | tag: String 29 | } 30 | 31 | enum AnimalType { 32 | DOG 33 | CAT 34 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/federation/exceptions/ExternalFieldNotFoundInBaseException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.federation.exceptions; 2 | 3 | import com.intuit.graphql.orchestrator.stitching.StitchingException; 4 | 5 | public class ExternalFieldNotFoundInBaseException extends StitchingException { 6 | 7 | private static final String MESSAGE_TEMPLATE = "Field %s is declared as external, but is not found in the originating type."; 8 | 9 | public ExternalFieldNotFoundInBaseException(String externalFieldName) { 10 | super(String.format(MESSAGE_TEMPLATE, externalFieldName)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/federation/exceptions/InvalidFieldSetReferenceException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.federation.exceptions; 2 | 3 | import com.intuit.graphql.orchestrator.stitching.StitchingException; 4 | 5 | public class InvalidFieldSetReferenceException extends StitchingException { 6 | 7 | private static final String ERR_INVALID_FIELD_REF_FORMAT = "Field '%s' does not exist in container '%s'."; 8 | 9 | public InvalidFieldSetReferenceException(String keyFieldName, String containerName) { 10 | super(String.format(ERR_INVALID_FIELD_REF_FORMAT, keyFieldName, containerName)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/test/resources/top_level/books-and-pets/mock-responses/get-petById-1to3.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "pet_0" : { 4 | "id": "pet-1", 5 | "name": "Charlie", 6 | "age": 2, 7 | "weight": 200, 8 | "purebred": true, 9 | "type": "DOG" 10 | }, 11 | "pet_1" : { 12 | "id": "pet-2", 13 | "name": "Milo", 14 | "age": 1, 15 | "weight": 20, 16 | "purebred": false, 17 | "type": "RABBIT" 18 | }, 19 | "pet_2" : { 20 | "id": "pet-3", 21 | "name": "Poppy", 22 | "age": 5, 23 | "weight": 100, 24 | "purebred": true, 25 | "type": "CAT" 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/utils/CreateGraphQLValueExceptionSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils 2 | 3 | import spock.lang.Specification 4 | 5 | class CreateGraphQLValueExceptionSpec extends Specification { 6 | def "CreateGraphQLValueException is instance of RunTimeException and sets the correct error message"() { 7 | given: 8 | CreateGraphQLValueException createGraphQLValueException = new CreateGraphQLValueException("test Message") 9 | expect: 10 | createGraphQLValueException instanceof RuntimeException 11 | createGraphQLValueException.message == "test Message" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/authorization/DownstreamQueryRedactorResult.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.authorization; 2 | 3 | import graphql.GraphqlErrorException; 4 | import graphql.language.Node; 5 | import java.util.List; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import lombok.NonNull; 9 | 10 | @Getter 11 | @AllArgsConstructor 12 | public class DownstreamQueryRedactorResult { 13 | 14 | private Node node; // nullable since transformer may return null in case field representing this node is denied 15 | 16 | @NonNull 17 | private List errors; 18 | 19 | boolean hasEmptySelectionSet; 20 | } -------------------------------------------------------------------------------- /src/test/java/com/intuit/graphql/orchestrator/testhelpers/ExecutionInputMatcher.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.testhelpers; 2 | 3 | import graphql.ExecutionInput; 4 | import lombok.AllArgsConstructor; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.mockito.ArgumentMatcher; 7 | 8 | @AllArgsConstructor 9 | public class ExecutionInputMatcher implements ArgumentMatcher { 10 | 11 | private ExecutionInput left; 12 | 13 | @Override 14 | public boolean matches(ExecutionInput right) { 15 | if (right != null) { 16 | return StringUtils.equals(left.getQuery(), right.getQuery()); 17 | } 18 | return false; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/resources/top_level/books-and-pets/mock-responses/get-pets.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "pets" : [ 4 | { 5 | "id": "pet-1", 6 | "name": "Charlie", 7 | "age": 2, 8 | "weight": 200, 9 | "purebred": true, 10 | "type": "DOG" 11 | }, 12 | { 13 | "id": "pet-2", 14 | "name": "Milo", 15 | "age": 1, 16 | "weight": 20, 17 | "purebred": false, 18 | "type": "RABBIT" 19 | }, 20 | { 21 | "id": "pet-3", 22 | "name": "Poppy", 23 | "age": 5, 24 | "weight": 100, 25 | "purebred": true, 26 | "type": "CAT" 27 | } 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/fieldresolver/QueryOperationFactory.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.fieldresolver; 2 | 3 | import graphql.language.OperationDefinition; 4 | import graphql.language.OperationDefinition.Operation; 5 | import graphql.language.SelectionSet; 6 | 7 | public class QueryOperationFactory { 8 | 9 | public OperationDefinition create(String operationName, SelectionSet selectionSet) { 10 | return OperationDefinition.newOperationDefinition() 11 | .name(operationName) 12 | .selectionSet(selectionSet) 13 | .operation(Operation.QUERY) 14 | .build(); 15 | 16 | } 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/schema/type/conflict/resolver/TypeConflictResolver.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema.type.conflict.resolver; 2 | 3 | import com.intuit.graphql.graphQL.TypeDefinition; 4 | import graphql.schema.GraphQLType; 5 | 6 | public interface TypeConflictResolver { 7 | 8 | /** 9 | * Resolves conflict 10 | * 11 | * @param conflictingType the new type ot resolve 12 | * @param existingType existingType in the schema 13 | * @return ResolvedType 14 | * @throws TypeConflictException if cannot resolve conflict. 15 | */ 16 | GraphQLType resolve(T conflictingType, GraphQLType existingType) throws TypeConflictException; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/authorization/DefaultFieldAuthorization.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.authorization; 2 | 3 | import static com.intuit.graphql.orchestrator.authorization.FieldAuthorizationResult.ALLOWED_FIELD_AUTH_RESULT; 4 | 5 | /** 6 | * This is the class used as default implementation of {@link FieldAuthorization} 7 | * if no custom implementation is provided. see {@link FieldAuthorization} for more information. 8 | */ 9 | public class DefaultFieldAuthorization implements FieldAuthorization { 10 | 11 | @Override 12 | public FieldAuthorizationResult authorize(FieldAuthorizationEnvironment fieldAuthorizationEnvironment) { 13 | return ALLOWED_FIELD_AUTH_RESULT; 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/schema/transform/ExplicitTypeResolver.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema.transform; 2 | 3 | import graphql.TypeResolutionEnvironment; 4 | import graphql.schema.GraphQLObjectType; 5 | import graphql.schema.TypeResolver; 6 | import java.util.Map; 7 | 8 | public class ExplicitTypeResolver implements TypeResolver { 9 | 10 | @Override 11 | public GraphQLObjectType getType(TypeResolutionEnvironment env) { 12 | Map objData = env.getObject(); 13 | if (objData.containsKey("__typename")) { 14 | return (GraphQLObjectType) env.getSchema().getType((String) objData.get("__typename")); 15 | } else { 16 | return null; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/schema/transform/ResolverArgumentListTypeNotSupported.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema.transform; 2 | 3 | import com.intuit.graphql.orchestrator.resolverdirective.ResolverDirectiveException; 4 | import com.intuit.graphql.orchestrator.xtext.FieldContext; 5 | 6 | public class ResolverArgumentListTypeNotSupported extends ResolverDirectiveException { 7 | 8 | private static final String MSG = "Resolver argument '%s' in '%s'. Field '%s' is a List type, which is not supported."; 9 | 10 | public ResolverArgumentListTypeNotSupported(String argumentName, FieldContext rootContext, String subField) { 11 | super(String.format(MSG, argumentName, rootContext.toString(), subField)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | 14 | **Describe the solution you'd like** 15 | 16 | 17 | 18 | **Describe alternatives you've considered** 19 | 20 | 21 | 22 | **Additional context** 23 | 24 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/schema/OperationSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema 2 | 3 | import spock.lang.Specification 4 | 5 | class OperationSpec extends Specification { 6 | 7 | def "test Operation Name"() { 8 | expect: 9 | Operation.QUERY.getName() == "Query" 10 | Operation.MUTATION.getName() == "Mutation" 11 | Operation.SUBSCRIPTION.getName() == "Subscription" 12 | } 13 | 14 | def "test asGraphQLObjectType"() { 15 | expect: 16 | Operation.QUERY.asGraphQLObjectType().name == "Query" 17 | Operation.MUTATION.asGraphQLObjectType().name == "Mutation" 18 | Operation.SUBSCRIPTION.asGraphQLObjectType().name == "Subscription" 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/com/intuit/graphql/orchestrator/testhelpers/TestFileLoader.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.testhelpers; 2 | 3 | import static com.intuit.graphql.orchestrator.testhelpers.JsonTestUtils.MAPPER; 4 | 5 | import com.fasterxml.jackson.core.type.TypeReference; 6 | import com.google.common.io.Resources; 7 | import java.io.IOException; 8 | import java.nio.charset.Charset; 9 | import java.util.Map; 10 | 11 | public class TestFileLoader { 12 | 13 | public static Map loadJsonAsMap(String jsonResourceFile) throws IOException { 14 | String json = Resources.toString(Resources.getResource(jsonResourceFile), Charset.defaultCharset()); 15 | return MAPPER.readValue(json, new TypeReference>() { }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/resources/top_level/user/user-schema.graphqls: -------------------------------------------------------------------------------- 1 | type Mutation { 2 | addUser(newUser: NewUserInput!): User! 3 | deleteUserById(id : ID!) : User 4 | } 5 | 6 | type Query { 7 | userById(id: ID!): User 8 | users: [User] 9 | } 10 | 11 | type User { 12 | id : ID! 13 | username : String! 14 | password : String! 15 | firstName: String! 16 | lastName: String! 17 | email: String 18 | phone: String 19 | userStatus: UserStatus 20 | } 21 | 22 | input NewUserInput { 23 | id : ID! 24 | username : String! 25 | password : String! 26 | firstName: String! 27 | lastName: String! 28 | email: String 29 | phone: String 30 | } 31 | 32 | enum UserStatus { 33 | PREACTIVE, 34 | ACTIVE, 35 | DEACTIVATED 36 | } -------------------------------------------------------------------------------- /src/test/resources/nested/books-pets-person/mock-responses/get-pets.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "person": { 4 | "pets": [ 5 | { 6 | "id": "pet-1", 7 | "name": "Charlie", 8 | "age": 2, 9 | "weight": 200, 10 | "purebred": true, 11 | "tag": "DOG" 12 | }, 13 | { 14 | "id": "pet-2", 15 | "name": "Milo", 16 | "age": 2, 17 | "weight": 20, 18 | "purebred": false, 19 | "tag": "RABBIT" 20 | }, 21 | { 22 | "id": "pet-3", 23 | "name": "Poppy", 24 | "age": 5, 25 | "weight": 500, 26 | "purebred": true, 27 | "tag": "CAT" 28 | } 29 | ] 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/federation/exceptions/BaseTypeNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.federation.exceptions; 2 | 3 | import static java.lang.String.format; 4 | 5 | import com.intuit.graphql.orchestrator.stitching.StitchingException; 6 | 7 | /** 8 | * This exception should be thrown if a service provider is extending a type but the base type is 9 | * not found. 10 | */ 11 | public class BaseTypeNotFoundException extends StitchingException { 12 | 13 | private static final String ERR_MSG = "Base type not found. typename=%s, serviceNamespace=%s"; 14 | 15 | public BaseTypeNotFoundException(String entityTypename, String serviceNamespace) { 16 | super(format(ERR_MSG, entityTypename, serviceNamespace)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/resolverdirective/ResolverArgumentPrematureLeafTypeSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective 2 | 3 | import com.intuit.graphql.orchestrator.xtext.FieldContext 4 | import spock.lang.Specification 5 | 6 | class ResolverArgumentPrematureLeafTypeSpec extends Specification { 7 | 8 | def "produces Correct Error Message"() { 9 | given: 10 | final ResolverArgumentPrematureLeafType error = new ResolverArgumentPrematureLeafType( 11 | "argName", "enumType", new FieldContext("rootObject", "rootField"), "tax") 12 | 13 | expect: 14 | error.message.contains("Resolver argument 'argName' in 'rootObject:rootField': Premature enumType found in field 'tax'.") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/resources/nested/books-pets-person/mock-responses/get-pets-via-field-resolver.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "person": { 4 | "pets_0": [ 5 | { 6 | "id": "pet-1", 7 | "name": "Charlie", 8 | "age": 2, 9 | "weight": 200, 10 | "purebred": true, 11 | "tag": "DOG" 12 | }, 13 | { 14 | "id": "pet-2", 15 | "name": "Milo", 16 | "age": 2, 17 | "weight": 20, 18 | "purebred": false, 19 | "tag": "RABBIT" 20 | }, 21 | { 22 | "id": "pet-3", 23 | "name": "Poppy", 24 | "age": 5, 25 | "weight": 500, 26 | "purebred": true, 27 | "tag": "CAT" 28 | } 29 | ] 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/resolverdirective/ResolverArgumentFieldRootObjectDoesNotExistSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective 2 | 3 | import com.intuit.graphql.orchestrator.xtext.FieldContext 4 | import spock.lang.Specification 5 | 6 | class ResolverArgumentFieldRootObjectDoesNotExistSpec extends Specification { 7 | 8 | def "produces Correct Error Message"() { 9 | given: 10 | final ResolverArgumentFieldRootObjectDoesNotExist error = new ResolverArgumentFieldRootObjectDoesNotExist( 11 | "argName", new FieldContext("rootObject", "rootField"), "tax") 12 | 13 | expect: 14 | error.message.contains("Resolver argument 'argName' in 'rootObject:rootField': field 'tax' does not exist in schema.") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | * Please create a bug report or feature request on the issue section. 4 | * Discuss your issue and get the high level design approved from one of the code owners. 5 | * Please read the [Code of Conduct](/CODE_OF_CONDUCT.md) before contributing to this project. 6 | * Please link your issues to corresponding Pull Requests, especially for larger changes. 7 | * All code changes should have a new/edited test! 8 | 9 | ## Formatting 10 | Please import and use either the provided [Eclipse Java style guide](./documents/style-guide-eclipse.xml) or 11 | the [IntelliJ Java style guide](./documents/style-guide-intellij.xml) into your project. 12 | 13 | * (IntelliJ) Preferences -> Code Style -> Click gear -> Import 14 | * (Eclipse) Preferences -> Java -> Code Style -> Formatter -> Import -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/schema/transform/DirectivesTransformer.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema.transform; 2 | 3 | import com.intuit.graphql.graphQL.DirectiveDefinition; 4 | import com.intuit.graphql.orchestrator.utils.XtextUtils; 5 | import com.intuit.graphql.orchestrator.xtext.XtextGraph; 6 | import java.util.Set; 7 | import java.util.stream.Collectors; 8 | 9 | public class DirectivesTransformer implements Transformer { 10 | 11 | @Override 12 | public XtextGraph transform(XtextGraph source) { 13 | Set directives = XtextUtils 14 | .getAllContentsOfType(DirectiveDefinition.class, source.getXtextResourceSet()) 15 | .collect(Collectors.toSet()); 16 | return source.transform(builder -> builder.directives(directives)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/common/ArgumentValueResolver.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.common; 2 | 3 | import graphql.execution.ValuesResolver; 4 | import graphql.language.Field; 5 | import graphql.schema.GraphQLFieldDefinition; 6 | import graphql.schema.GraphQLSchema; 7 | import java.util.Map; 8 | import lombok.AllArgsConstructor; 9 | 10 | @AllArgsConstructor 11 | public class ArgumentValueResolver { 12 | 13 | public Map resolve(GraphQLSchema graphQLSchema, GraphQLFieldDefinition fieldDefinition, 14 | Field field, Map queryVariables) { 15 | 16 | ValuesResolver valuesResolver = new ValuesResolver(); 17 | return valuesResolver.getArgumentValues(graphQLSchema.getCodeRegistry(), fieldDefinition.getArguments(), 18 | field.getArguments(), queryVariables); 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/utils/ExtendedScalarsSupport.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils; 2 | 3 | import graphql.scalars.ExtendedScalars; 4 | import graphql.schema.GraphQLScalarType; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class ExtendedScalarsSupport { 9 | 10 | public static final List GRAPHQL_EXTENDED_SCALARS = new ArrayList<>(); 11 | 12 | static { 13 | GRAPHQL_EXTENDED_SCALARS.add(ExtendedScalars.GraphQLBigDecimal); 14 | GRAPHQL_EXTENDED_SCALARS.add(ExtendedScalars.GraphQLBigInteger); 15 | GRAPHQL_EXTENDED_SCALARS.add(ExtendedScalars.GraphQLByte); 16 | GRAPHQL_EXTENDED_SCALARS.add(ExtendedScalars.GraphQLChar); 17 | GRAPHQL_EXTENDED_SCALARS.add(ExtendedScalars.GraphQLShort); 18 | GRAPHQL_EXTENDED_SCALARS.add(ExtendedScalars.GraphQLLong); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/utils/IntrospectionUtil.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils; 2 | 3 | import static graphql.introspection.Introspection.SchemaMetaFieldDef; 4 | import static graphql.introspection.Introspection.TypeMetaFieldDef; 5 | import static graphql.introspection.Introspection.TypeNameMetaFieldDef; 6 | 7 | import graphql.introspection.Introspection; 8 | import graphql.language.Field; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | 12 | public class IntrospectionUtil { 13 | 14 | public static final Field __typenameField = 15 | Field.newField().name(Introspection.TypeNameMetaFieldDef.getName()).build(); 16 | 17 | public static final List INTROSPECTION_FIELDS = 18 | Arrays.asList( 19 | SchemaMetaFieldDef.getName(), TypeMetaFieldDef.getName(), TypeNameMetaFieldDef.getName()); 20 | } 21 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/resolverdirective/ResolverArgumentFieldNotInSchemaSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective 2 | 3 | import com.intuit.graphql.orchestrator.xtext.FieldContext 4 | import spock.lang.Specification 5 | 6 | class ResolverArgumentFieldNotInSchemaSpec extends Specification { 7 | 8 | def "produces Correct Error Message"() { 9 | given: 10 | final ResolverArgumentFieldNotInSchema error = new ResolverArgumentFieldNotInSchema( 11 | "argName", new FieldContext("rootObject", "rootField"), 12 | new FieldContext("parentObject", "parentField")) 13 | 14 | expect: 15 | error.message.contains( 16 | "Resolver argument 'argName' in 'rootObject:rootField': field 'parentField' in InputType 'parentObject' does not exist in schema.") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/resolverdirective/ExternalTypeNotfoundException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective; 2 | 3 | import com.intuit.graphql.orchestrator.stitching.StitchingException; 4 | 5 | public class ExternalTypeNotfoundException extends StitchingException { 6 | 7 | private static final String MESSAGE_TEMPLATE = 8 | "External type not found. " 9 | + "serviceName=%s, " 10 | + "parentTypeName=%s, " 11 | + "fieldName=%s, " 12 | + "placeHolderTypeDescription=%s"; 13 | 14 | public ExternalTypeNotfoundException( 15 | String serviceName, 16 | String parentTypeName, 17 | String fieldName, 18 | String placeHolderTypeDescription) { 19 | super(String.format(MESSAGE_TEMPLATE, serviceName, parentTypeName, fieldName, 20 | placeHolderTypeDescription)); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/authorization/FieldAuthorizationEnvironment.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.authorization; 2 | 3 | import graphql.language.Field; 4 | import graphql.schema.FieldCoordinates; 5 | import java.util.List; 6 | import java.util.Map; 7 | import lombok.Builder; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.Getter; 10 | import lombok.NonNull; 11 | 12 | @Builder 13 | @Getter 14 | @EqualsAndHashCode 15 | public class FieldAuthorizationEnvironment { 16 | @NonNull @EqualsAndHashCode.Include 17 | private FieldCoordinates fieldCoordinates; 18 | @NonNull @EqualsAndHashCode.Exclude 19 | private Field field; 20 | @NonNull @EqualsAndHashCode.Exclude 21 | private Object authData; 22 | @NonNull @EqualsAndHashCode.Exclude 23 | private Map argumentValues; 24 | @NonNull @EqualsAndHashCode.Exclude 25 | private List path; 26 | } -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/schema/SchemaParseExceptionSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema 2 | 3 | import spock.lang.Specification 4 | 5 | class SchemaParseExceptionSpec extends Specification { 6 | 7 | def "Exception message is correctly formed for exception with msg argument"() { 8 | given: 9 | SchemaParseException schemaParseException = new SchemaParseException("test Message") 10 | expect: 11 | schemaParseException.message == "test Message" 12 | } 13 | 14 | def "Exception message is correctly formed for exception with msg and throwable argument"() { 15 | given: 16 | SchemaParseException schemaParseException = new SchemaParseException("test Message", new Throwable()) 17 | expect: 18 | schemaParseException.message == "test Message" 19 | schemaParseException.cause != null 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/metadata/RenamedMetadata.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.metadata; 2 | 3 | import com.intuit.graphql.orchestrator.ServiceProvider; 4 | import lombok.Getter; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | @Getter 10 | public class RenamedMetadata { 11 | 12 | private final ServiceProvider serviceProvider; 13 | Map originalTypeNamesByRenamedName; 14 | Map originalFieldNamesByRenamedName; 15 | 16 | public RenamedMetadata(ServiceProvider serviceProvider) { 17 | this.serviceProvider = serviceProvider; 18 | this.originalTypeNamesByRenamedName = new HashMap<>(); 19 | this.originalFieldNamesByRenamedName = new HashMap<>(); 20 | } 21 | 22 | public boolean containsRenamedFields() { 23 | return !this.getOriginalFieldNamesByRenamedName().isEmpty(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/integration/DownstreamGraphqlErrorMappingSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.integration 2 | 3 | import helpers.BaseIntegrationTestSpecification 4 | 5 | class DownstreamGraphqlErrorMappingSpec extends BaseIntegrationTestSpecification{ 6 | 7 | // subtree batch transformer 8 | 9 | //single 10 | def "200 with errors with path (single result)"(){} 11 | def "200 with errors with path and partial data (single result)"(){} 12 | def "200 with error with no path (single result)"(){} 13 | def "200 with error with no path and partial data (single result)"(){} 14 | 15 | //batch 16 | def "200 with errors with path (batch result)"(){} 17 | def "200 with errors with path and partial data (batch result)"(){} 18 | def "200 with error with no path (batch result)"(){} 19 | def "200 with error with no path and partial data (batch result)"(){} 20 | } 21 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/integration/DownstreamRestErrorMappingSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.integration 2 | 3 | import helpers.BaseIntegrationTestSpecification 4 | 5 | class DownstreamRestErrorMappingSpec extends BaseIntegrationTestSpecification { 6 | 7 | //Default Batch Result Transformer 8 | 9 | //single 10 | def "200 with errors with path (single result)"(){} 11 | def "200 with errors with path and partial data (single result)"(){} 12 | def "200 with error with no path (single result)"(){} 13 | def "200 with error with no path and partial data (single result)"(){} 14 | 15 | //batch 16 | def "200 with errors with path (batch result)"(){} 17 | def "200 with errors with path and partial data (batch result)"(){} 18 | def "200 with error with no path (batch result)"(){} 19 | def "200 with error with no path and partial data (batch result)"(){} 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/batch/BatchLoaderExecutionHooksSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.batch 2 | 3 | import graphql.ExecutionInput 4 | import graphql.GraphQLContext 5 | import spock.lang.Specification 6 | 7 | class BatchLoaderExecutionHooksSpec extends Specification { 8 | 9 | def "test Default Methods"() { 10 | given: 11 | final BatchLoaderExecutionHooks hooks = new BatchLoaderExecutionHooks() {} 12 | 13 | GraphQLContext context = GraphQLContext.newContext().build() 14 | 15 | when: 16 | hooks.onBatchLoadEnd(context, Collections.singletonList("")) 17 | hooks.onBatchLoadStart(context, Collections.singletonList("")) 18 | hooks.onExecutionInput(context, Mock(ExecutionInput.class)) 19 | hooks.onQueryResult(context, Collections.emptyMap()) 20 | 21 | then: 22 | noExceptionThrown() 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/utils/RenameDirectiveUtil.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils; 2 | 3 | 4 | import graphql.language.Field; 5 | import org.apache.commons.lang3.StringUtils; 6 | 7 | import java.util.Arrays; 8 | 9 | public class RenameDirectiveUtil { 10 | 11 | private RenameDirectiveUtil(){} 12 | 13 | public static Field convertGraphqlFieldWithOriginalName(Field renamedField, String originalName) { 14 | String alias = (StringUtils.isNotEmpty(renamedField.getAlias())) ? renamedField.getAlias() : renamedField.getName(); 15 | 16 | return renamedField.transform( builder -> 17 | builder.alias(alias).name(originalName) 18 | ); 19 | } 20 | 21 | public static String getRenameKey(String parentTypeName, String aliasName, boolean isOperation) { 22 | return (isOperation) ? aliasName :StringUtils.join(Arrays.asList(parentTypeName, aliasName), "-"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/schema/RuntimeGraphSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema 2 | 3 | import graphql.schema.GraphQLDirective 4 | import graphql.schema.GraphQLObjectType 5 | import graphql.schema.GraphQLType 6 | import spock.lang.Specification 7 | 8 | class RuntimeGraphSpec extends Specification { 9 | RuntimeGraph runtimeGraph 10 | 11 | def setup() { 12 | runtimeGraph = RuntimeGraph.newBuilder() 13 | .additionalTypes(new HashMap()) 14 | .additionalDirectives(new HashSet()) 15 | .operationMap(new HashMap()) 16 | .build() 17 | } 18 | 19 | def "runTimeGraph is formed"() { 20 | expect: 21 | runtimeGraph != null 22 | runtimeGraph.getAdditionalTypes().size() == 0 23 | runtimeGraph.getAddtionalDirectives().size() == 0 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/intuit/graphql/orchestrator/testhelpers/JsonTestUtils.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.testhelpers; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import java.util.Map; 6 | 7 | public class JsonTestUtils { 8 | 9 | static final ObjectMapper MAPPER = new ObjectMapper(); 10 | 11 | public static String toJson(Object map) { 12 | try { 13 | return MAPPER.writeValueAsString(map); 14 | } catch (JsonProcessingException e) { 15 | throw new IllegalArgumentException("Input map cannot be converted to json", e); 16 | } 17 | } 18 | 19 | public static Map jsonToMap(String jsonString) { 20 | try { 21 | return MAPPER.readValue(jsonString, Map.class); 22 | } catch (JsonProcessingException e) { 23 | throw new IllegalArgumentException("json string cannot be converted to Map", e); 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/batch/DataLoaderKeyUtil.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.batch; 2 | 3 | import com.intuit.graphql.orchestrator.schema.transform.FieldResolverContext; 4 | import org.apache.commons.lang3.StringUtils; 5 | 6 | public class DataLoaderKeyUtil { 7 | 8 | public static final String DELIMITER = ":"; 9 | 10 | private DataLoaderKeyUtil() {} 11 | 12 | public static String createDataLoaderKey(String... tokens) { 13 | return StringUtils.join(tokens, DELIMITER); 14 | } 15 | 16 | public static String createDataLoaderKeyFrom(FieldResolverContext fieldResolverContext) { 17 | String serviceNamespace = fieldResolverContext.getServiceNamespace(); 18 | String parentTypename = fieldResolverContext.getParentTypeDefinition().getName(); 19 | String fieldName = fieldResolverContext.getFieldDefinition().getName(); 20 | return createDataLoaderKey(serviceNamespace, parentTypename, fieldName); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/resources/top_level/starwars/schema-starwars.graphqls: -------------------------------------------------------------------------------- 1 | schema { 2 | query: QueryType 3 | } 4 | 5 | type QueryType { 6 | 7 | } 8 | 9 | extend type QueryType { 10 | human(id : String) : Human 11 | characters: [Character!]! 12 | } 13 | 14 | extend type QueryType { 15 | droid(id: ID!): Droid 16 | } 17 | 18 | extend type QueryType { 19 | hero(episode: Episode): Character 20 | } 21 | 22 | enum Episode { 23 | NEWHOPE 24 | EMPIRE 25 | JEDI 26 | } 27 | 28 | interface Character { 29 | id: ID! 30 | name: String! 31 | appearsIn: [Episode]! 32 | friends: [Character] 33 | } 34 | 35 | type Human implements Character { 36 | id: ID! 37 | name: String! 38 | appearsIn: [Episode]! 39 | friends: [Character] 40 | homePlanet: String 41 | } 42 | 43 | type Droid implements Character { 44 | id: ID! 45 | name: String! 46 | appearsIn: [Episode]! 47 | friends: [Character] 48 | primaryFunction: String 49 | } 50 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/schema/transform/ExplicitTypeResolverSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema.transform 2 | 3 | import graphql.TypeResolutionEnvironment 4 | import graphql.schema.GraphQLObjectType 5 | import spock.lang.Specification 6 | 7 | class ExplicitTypeResolverSpec extends Specification { 8 | private ExplicitTypeResolver explicitTypeResolver 9 | def setup() { 10 | explicitTypeResolver = new ExplicitTypeResolver() 11 | } 12 | def "Explicit Type Resolver with no typename in the objectMap"() { 13 | given: 14 | Map objData = new HashMap<>() 15 | TypeResolutionEnvironment typeResolutionEnvironment = new TypeResolutionEnvironment(objData, new HashMap(), null, null, null, null) 16 | GraphQLObjectType graphQLObjectType = explicitTypeResolver.getType(typeResolutionEnvironment) 17 | 18 | expect: 19 | graphQLObjectType == null 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/groovy/helpers/SchemaTestUtil.groovy: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import graphql.schema.GraphQLSchema 4 | import graphql.schema.idl.RuntimeWiring 5 | import graphql.schema.idl.SchemaGenerator 6 | import graphql.schema.idl.SchemaParser 7 | import graphql.schema.idl.errors.SchemaProblem 8 | import spock.lang.Specification 9 | 10 | class SchemaTestUtil extends Specification { 11 | 12 | static RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring().build() 13 | 14 | static defaultOptions = SchemaGenerator.Options.defaultOptions() 15 | 16 | static SchemaParser schemaParser = new SchemaParser() 17 | 18 | static GraphQLSchema createGraphQLSchema(String sdl) { 19 | try { 20 | def registry = schemaParser.parse(sdl) 21 | return new SchemaGenerator().makeExecutableSchema(defaultOptions, registry, runtimeWiring) 22 | } catch (SchemaProblem e) { 23 | assert false: "Failed to create schema: ${e}" 24 | return null 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/federation/exceptions/IncorrectDirectiveArgumentSizeException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.federation.exceptions; 2 | 3 | import com.intuit.graphql.orchestrator.stitching.StitchingException; 4 | 5 | public class IncorrectDirectiveArgumentSizeException extends StitchingException { 6 | private static final String ERROR_EXACT_MSG = "%s Directive for '%s' must have %d arguments"; 7 | private static final String ERROR_ATLEAST_MSG = "%s Directive for '%s' must have at least %d arguments"; 8 | 9 | public IncorrectDirectiveArgumentSizeException(String directiveName,String entity, int desiredArgumentSize) { 10 | this(directiveName, entity, desiredArgumentSize, true); 11 | } 12 | 13 | public IncorrectDirectiveArgumentSizeException(String directiveName,String entity, int desiredArgumentSize, boolean exactSize) { 14 | super(String.format(((exactSize) ? ERROR_EXACT_MSG : ERROR_ATLEAST_MSG), directiveName, entity, desiredArgumentSize)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/integration/DownstreamEntityErrorMappingSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.integration 2 | 3 | import helpers.BaseIntegrationTestSpecification 4 | 5 | class DownstreamEntityErrorMappingSpec extends BaseIntegrationTestSpecification { 6 | 7 | //TODO Creating empty file to be implemented in different story 8 | //downstream uses federation entity batch transformer 9 | 10 | //single 11 | def "200 with errors with path (single result)"(){} 12 | def "200 with errors with path and partial data (single result)"(){} 13 | def "200 with error with no path (single result)"(){} 14 | def "200 with error with no path and partial data (single result)"(){} 15 | 16 | //batch 17 | def "200 with errors with path (batch result)"(){} 18 | def "200 with errors with path and partial data (batch result)"(){} 19 | def "200 with error with no path (batch result)"(){} 20 | def "200 with error with no path and partial data (batch result)"(){} 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/utils/XtextGraphUtils.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils; 2 | 3 | import com.intuit.graphql.orchestrator.xtext.DataFetcherContext; 4 | import com.intuit.graphql.orchestrator.xtext.FieldContext; 5 | import com.intuit.graphql.orchestrator.xtext.XtextGraph; 6 | import com.intuit.graphql.orchestrator.xtext.UnifiedXtextGraph; 7 | 8 | public class XtextGraphUtils { 9 | 10 | private XtextGraphUtils() {} 11 | 12 | public static void addToCodeRegistry(FieldContext fieldContext, DataFetcherContext dataFetcherContext, 13 | XtextGraph sourceXtextGraph) { 14 | 15 | sourceXtextGraph 16 | .getCodeRegistry() 17 | .put(fieldContext, dataFetcherContext); 18 | } 19 | 20 | public static void addToCodeRegistry(FieldContext fieldContext, DataFetcherContext dataFetcherContext, 21 | UnifiedXtextGraph sourceUnifiedXtextGraph) { 22 | sourceUnifiedXtextGraph 23 | .getCodeRegistry() 24 | .put(fieldContext, dataFetcherContext); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/com/intuit/graphql/orchestrator/SelectionSetUtil.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator; 2 | 3 | import graphql.language.Field; 4 | import graphql.language.SelectionSet; 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | public class SelectionSetUtil { 9 | 10 | public static Field getFieldByPath(List pathList, SelectionSet selectionSet) { 11 | SelectionSet currentSelectionSet = selectionSet; 12 | 13 | Field currentField = null; 14 | for (String fieldName : pathList) { 15 | if (currentSelectionSet == null) return null; 16 | 17 | Optional optionalField = 18 | currentSelectionSet.getSelectionsOfType(Field.class).stream() 19 | .filter(field -> field.getName().equals(fieldName)) 20 | .findFirst(); 21 | 22 | if (!optionalField.isPresent()) return null; 23 | 24 | currentField = optionalField.get(); 25 | currentSelectionSet = currentField.getSelectionSet(); 26 | } 27 | 28 | return currentField; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/authorization/FieldAuthorizationResultSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.authorization 2 | 3 | import graphql.GraphqlErrorException 4 | import spock.lang.Specification 5 | 6 | class FieldAuthorizationResultSpec extends Specification { 7 | 8 | GraphqlErrorException testGraphqlErrorException 9 | 10 | def setup() { 11 | 12 | def extensionsMap = [ 13 | ext1: "value1", 14 | ext2: "value2" 15 | ] 16 | 17 | testGraphqlErrorException = GraphqlErrorException.newErrorException() 18 | .message("testMessage") 19 | .extensions(extensionsMap) 20 | .path(["a","b","c"]) 21 | .build() 22 | } 23 | 24 | def "creates correct denied result"() { 25 | when: 26 | def actual = FieldAuthorizationResult.createDeniedResult(testGraphqlErrorException) 27 | 28 | then: 29 | actual.isAllowed() == false 30 | actual.getGraphqlErrorException() == testGraphqlErrorException 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/resources/top_level/books-and-pets/mock-responses/get-books-missing-field-link.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "books": [ 4 | { 5 | "pageCount": "2000", 6 | "isFamilyFriendly": true, 7 | "author": { 8 | "id": "author-1", 9 | "lastName": "AuthorOne" 10 | }, 11 | "name": "GraphQL Advanced Stitching", 12 | "weight": 1.8, 13 | "id": "book-1" 14 | }, 15 | { 16 | "pageCount": "100", 17 | "isFamilyFriendly": true, 18 | "author": { 19 | "id": "author-2", 20 | "lastName": "AuthorTwo" 21 | }, 22 | "name": "The Recursion", 23 | "weight": 1.4, 24 | "id": "book-2" 25 | }, 26 | { 27 | "pageCount": "223", 28 | "isFamilyFriendly": false, 29 | "author": { 30 | "id": "author-3", 31 | "lastName": "AuthorThree", 32 | "petId": null 33 | }, 34 | "name": "Spring In Action", 35 | "weight": 1.2, 36 | "id": "book-3" 37 | } 38 | ] 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/schema/SchemaParseException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema; 2 | 3 | import com.intuit.graphql.orchestrator.stitching.StitchingException; 4 | import java.util.List; 5 | import java.util.Set; 6 | import org.apache.commons.lang3.StringUtils; 7 | import org.eclipse.xtext.validation.Issue; 8 | 9 | /** 10 | * An exception thrown when an error occur during schema parsing. 11 | */ 12 | public class SchemaParseException extends StitchingException { 13 | 14 | private static final String FORMATTER = "Issues found while parsing the schema. \n %s \n These parsing issues were found in the following files. \n %s"; 15 | 16 | public SchemaParseException(String msg, Throwable t) { 17 | super(msg, t); 18 | } 19 | 20 | public SchemaParseException(String msg) { 21 | super(msg); 22 | } 23 | 24 | public SchemaParseException(List issues, Set files) { 25 | super(String.format(FORMATTER, 26 | StringUtils.join(issues, System.lineSeparator()), StringUtils.join(issues, System.lineSeparator()))); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/xtext/XtextScalarsSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.xtext 2 | 3 | import spock.lang.Specification 4 | 5 | import static com.intuit.graphql.orchestrator.xtext.XtextScalars.* 6 | 7 | class XtextScalarsSpec extends Specification { 8 | 9 | def "test Does Not Return Same Instance"() { 10 | expect: 11 | !newBigDecimalType().is(newBigDecimalType()) 12 | !newBigIntType().is(newBigIntType()) 13 | !newBooleanType().is(newBooleanType()) 14 | !newByteType().is(newByteType()) 15 | !newCharType().is(newCharType()) 16 | !newFloatType().is(newFloatType()) 17 | !newIntType().is(newIntType()) 18 | !newIdType().is(newIdType()) 19 | !newStringType().is(newStringType()) 20 | !newLongType().is(newLongType()) 21 | !newShortType().is(newShortType()) 22 | !newFieldSetType().is(newFieldSetType()) 23 | } 24 | 25 | def "test Standard Scalar Map"() { 26 | expect: 27 | XtextScalars.STANDARD_SCALARS.size() == 12 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/federation/validators/ExternalValidator.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.federation.validators; 2 | 3 | import com.intuit.graphql.graphQL.FieldDefinition; 4 | import com.intuit.graphql.orchestrator.federation.EntityTypeMerger; 5 | import com.intuit.graphql.orchestrator.federation.exceptions.ExternalFieldNotFoundInBaseException; 6 | 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | 10 | import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.getFieldDefinitions; 11 | 12 | public class ExternalValidator { 13 | public void validatePostMerge(EntityTypeMerger.EntityMergingContext entityMergingContext, FieldDefinition fieldDefinition) { 14 | List baseFieldNames = getFieldDefinitions(entityMergingContext.getBaseType()).stream() 15 | .map(FieldDefinition::getName).collect(Collectors.toList()); 16 | 17 | if(!baseFieldNames.contains(fieldDefinition.getName())) { 18 | throw new ExternalFieldNotFoundInBaseException(fieldDefinition.getName()); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/fieldresolver/TypenameInjector.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.fieldresolver; 2 | 3 | import com.intuit.graphql.orchestrator.batch.QueryOperationModifier; 4 | import graphql.language.FragmentDefinition; 5 | import graphql.language.OperationDefinition; 6 | import graphql.schema.GraphQLSchema; 7 | 8 | import java.util.Map; 9 | 10 | public class TypenameInjector { 11 | 12 | // consider moving QueryOperationModifier code here if the sole purpose is to inject typename 13 | private final QueryOperationModifier queryOperationModifier = new QueryOperationModifier(); 14 | 15 | 16 | public OperationDefinition process(OperationDefinition operationDefinition, 17 | GraphQLSchema graphQLSchema, 18 | Map fragmentsByName, 19 | Map variables) { 20 | 21 | return queryOperationModifier.modifyQuery(graphQLSchema, operationDefinition, fragmentsByName, variables); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/schema/transform/DomainTypesTransformer.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema.transform; 2 | 3 | import com.intuit.graphql.orchestrator.utils.XtextUtils; 4 | import com.intuit.graphql.orchestrator.xtext.XtextGraph; 5 | import java.util.Set; 6 | import org.apache.commons.collections4.CollectionUtils; 7 | 8 | public class DomainTypesTransformer implements Transformer { 9 | 10 | public static final String DELIMITER = "_"; 11 | 12 | @Override 13 | public XtextGraph transform(XtextGraph xtextGraph) { 14 | 15 | Set domainTypes = xtextGraph.getServiceProvider().domainTypes(); 16 | if (CollectionUtils.isNotEmpty(domainTypes)) { 17 | String namespace = xtextGraph.getServiceProvider().getNameSpace(); 18 | XtextUtils.getAllTypes(xtextGraph.getXtextResourceSet()) 19 | .filter(typeDefinition -> domainTypes.contains(typeDefinition.getName())) 20 | .forEach(domainType -> domainType.setName(namespace + DELIMITER + domainType.getName())); 21 | } 22 | return xtextGraph; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/resources/top_level/user/mock-responses/get-users.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "users": [ 4 | { 5 | "firstName": "Delilah", 6 | "lastName": "Hadfield", 7 | "password": "guesstheword", 8 | "userStatus": "ACTIVE", 9 | "phone": "3436293059", 10 | "id": "user-1", 11 | "email": "delilah.hadfield@mail.com", 12 | "username": "delilah.hadfield" 13 | }, 14 | { 15 | "firstName": "Huong", 16 | "lastName": "Seamon", 17 | "password": "idontknow", 18 | "userStatus": "DEACTIVATED", 19 | "phone": "7676293059", 20 | "id": "user-2", 21 | "email": "huong.seamon@mail.com", 22 | "username": "huong.seamon" 23 | }, 24 | { 25 | "firstName": "Geraldine", 26 | "lastName": "Gower", 27 | "password": "thenorthremembers", 28 | "userStatus": "PREACTIVE", 29 | "phone": "5856293059", 30 | "id": "user-3", 31 | "email": "geraldine.gower@mail.com", 32 | "username": "geraldine.gower" 33 | } 34 | ] 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/authorization/FieldAuthorizationResult.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.authorization; 2 | 3 | import graphql.GraphqlErrorException; 4 | import java.util.Objects; 5 | import lombok.Getter; 6 | 7 | public class FieldAuthorizationResult { 8 | 9 | public static final FieldAuthorizationResult ALLOWED_FIELD_AUTH_RESULT = 10 | new FieldAuthorizationResult(true, null); 11 | 12 | @Getter private final boolean isAllowed; 13 | @Getter private final GraphqlErrorException graphqlErrorException; 14 | 15 | private FieldAuthorizationResult(boolean isAllowed, GraphqlErrorException graphqlErrorException) { 16 | this.isAllowed = isAllowed; 17 | this.graphqlErrorException = graphqlErrorException; 18 | } 19 | 20 | public static FieldAuthorizationResult createDeniedResult(GraphqlErrorException graphqlErrorException) { 21 | Objects.requireNonNull(graphqlErrorException, "an instance of GraphqlErrorException is " 22 | + "required to create a denied FieldAuthorizationResult"); 23 | return new FieldAuthorizationResult(false, graphqlErrorException); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/resolverdirective/ResolverArgumentNotAFieldOfParentException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective; 2 | 3 | import static java.lang.String.format; 4 | 5 | public class ResolverArgumentNotAFieldOfParentException extends ResolverDirectiveException { 6 | 7 | private static final String ERR_MSG = "Resolver argument value %s should be a reference " 8 | + "to a field in Parent Type %s"; 9 | 10 | private static final String ERR_MSG2 = "'%s' is not a field of parent type. " 11 | + "serviceName=%s, " 12 | + "parentTypeName=%s, " 13 | + "fieldName=%s, "; 14 | 15 | public ResolverArgumentNotAFieldOfParentException(String resolverArgValue, String parentTypeName) { 16 | super(format(ERR_MSG, resolverArgValue, parentTypeName)); 17 | } 18 | 19 | public ResolverArgumentNotAFieldOfParentException(String reqdFieldName, String serviceName, 20 | String parentTypeName, String fieldName) { 21 | super(format(ERR_MSG2, reqdFieldName, serviceName, parentTypeName, fieldName)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/resources/top_level/books-and-pets/mock-responses/get-books.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "books": [ 4 | { 5 | "pageCount": "2000", 6 | "isFamilyFriendly": true, 7 | "author": { 8 | "id": "author-1", 9 | "lastName": "AuthorOne", 10 | "petId": "pet-1" 11 | }, 12 | "name": "GraphQL Advanced Stitching", 13 | "weight": 1.8, 14 | "id": "book-1" 15 | }, 16 | { 17 | "pageCount": "100", 18 | "isFamilyFriendly": true, 19 | "author": { 20 | "id": "author-2", 21 | "lastName": "AuthorTwo", 22 | "petId": "pet-2" 23 | }, 24 | "name": "The Recursion", 25 | "weight": 1.4, 26 | "id": "book-2" 27 | }, 28 | { 29 | "pageCount": "223", 30 | "isFamilyFriendly": false, 31 | "author": { 32 | "id": "author-3", 33 | "lastName": "AuthorThree", 34 | "petId": "pet-3" 35 | }, 36 | "name": "Spring In Action", 37 | "weight": 1.2, 38 | "id": "book-3" 39 | } 40 | ] 41 | } 42 | } -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/resolverdirective/ResolverArgumentDirectiveSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective 2 | 3 | import com.intuit.graphql.orchestrator.TestHelper 4 | import graphql.Scalars 5 | import graphql.schema.GraphQLArgument 6 | import spock.lang.Specification 7 | 8 | class ResolverArgumentDirectiveSpec extends Specification { 9 | 10 | def "creates From GraphQL Argument"() { 11 | given: 12 | String schema = ''' 13 | schema { query: Query } 14 | type Query { a(arg: Int @resolver(field: "a.b.c")): Int } 15 | directive @resolver(field: String) on ARGUMENT_DEFINITION 16 | ''' 17 | final GraphQLArgument arg = TestHelper.schema(schema).getQueryType().getFieldDefinition("a") 18 | .getArgument("arg") 19 | 20 | when: 21 | final ResolverArgumentDirective result = ResolverArgumentDirective.fromGraphQLArgument(arg) 22 | 23 | then: 24 | result.getArgumentName() == "arg" 25 | result.getField() == "a.b.c" 26 | result.getInputType() == Scalars.GraphQLInt 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/utils/DescriptionUtilsSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils 2 | 3 | import spock.lang.Specification 4 | 5 | import static com.intuit.graphql.orchestrator.utils.DescriptionUtils.attachNamespace 6 | import static com.intuit.graphql.orchestrator.utils.DescriptionUtils.mergeDescriptions 7 | 8 | class DescriptionUtilsSpec extends Specification { 9 | 10 | def "attach Name Space Test"(){ 11 | expect: 12 | attachNamespace("ns","desc") == "[ns] desc" 13 | attachNamespace("ns",null) == "[ns]" 14 | } 15 | 16 | def "merge Descrtiption Test"(){ 17 | given: 18 | String firstMerge = mergeDescriptions("[ns1] desc1", "[ns2] desc2") 19 | 20 | expect: 21 | firstMerge == "[ns1,ns2] desc1, desc2" 22 | mergeDescriptions(firstMerge, firstMerge) == "[ns1,ns2,ns1,ns2] desc1, desc2, desc1, desc2" 23 | mergeDescriptions(firstMerge, "[ns3] desc3") == "[ns1,ns2,ns3] desc1, desc2, desc3" 24 | mergeDescriptions(firstMerge, "[ns3]") == "[ns1,ns2,ns3] desc1, desc2" 25 | mergeDescriptions(firstMerge, "") == firstMerge 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/fieldresolver/FieldResolverValidator.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.fieldresolver; 2 | 3 | import com.intuit.graphql.orchestrator.resolverdirective.ResolverArgumentNotAFieldOfParentException; 4 | import com.intuit.graphql.orchestrator.schema.transform.FieldResolverContext; 5 | import java.util.Objects; 6 | 7 | public class FieldResolverValidator { 8 | 9 | public static void validateRequiredFields(FieldResolverContext fieldResolverContext) { 10 | if (Objects.nonNull(fieldResolverContext.getRequiredFields())) { 11 | fieldResolverContext.getRequiredFields().forEach(reqdFieldName -> { 12 | if (!fieldResolverContext.getParentTypeFields().containsKey(reqdFieldName)) { 13 | String serviceName = fieldResolverContext.getServiceNamespace(); 14 | String parentTypeName = fieldResolverContext.getParentTypename(); 15 | String fieldResolverName = fieldResolverContext.getFieldName(); 16 | throw new ResolverArgumentNotAFieldOfParentException(reqdFieldName, serviceName, 17 | parentTypeName, fieldResolverName); 18 | } 19 | }); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/resolverdirective/ResolverArgumentFieldRootObjectDoesNotExist.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective; 2 | 3 | import com.intuit.graphql.orchestrator.xtext.FieldContext; 4 | 5 | /** 6 | * Exception will occur if a any sub-path in a resolve argument field does not exist in the schema (starting from the 7 | * query root). 8 | * 9 | * Example: 10 | * 11 | * {@code @resolver(field: "a.b.c")} 12 | * 13 | * In "a.b.c", "a" must exist as a FieldDefinition under query root, "b" must exist as a FieldDefinition under the 14 | * ObjectTypeDefinition found for "a", and "c" must exist as a FieldDefinition under the ObjectTypeDefinition found for 15 | * "b". 16 | */ 17 | public class ResolverArgumentFieldRootObjectDoesNotExist extends ResolverDirectiveException { 18 | 19 | private static final String MSG = "Resolver argument '%s' in '%s': field '%s' does not exist in schema."; 20 | 21 | public ResolverArgumentFieldRootObjectDoesNotExist(final String argumentName, final FieldContext rootContext, 22 | final String fieldName) { 23 | super(String.format(MSG, argumentName, rootContext.toString(), fieldName)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/intuit/graphql/orchestrator/ExecutionInputTestUtil.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator; 2 | 3 | import static com.intuit.graphql.orchestrator.TestHelper.TEST_MAPPER; 4 | 5 | import graphql.ExecutionInput; 6 | import java.io.IOException; 7 | import java.util.Map; 8 | import java.util.Objects; 9 | import java.util.function.UnaryOperator; 10 | 11 | public class ExecutionInputTestUtil { 12 | 13 | public static UnaryOperator builderFunc(String graphqlQueryStr) throws IOException { 14 | return builder -> { 15 | Map queryMap = null; 16 | try { 17 | queryMap = TEST_MAPPER.readValue(graphqlQueryStr, Map.class); 18 | Objects.requireNonNull(queryMap.get("query")); 19 | builder.query((String)queryMap.get("query")); 20 | if (Objects.nonNull(queryMap.get("variables"))) builder.variables((Map)queryMap.get("variables")); 21 | if (Objects.nonNull(queryMap.get("operationName"))) builder.operationName((String)queryMap.get("operationName")); 22 | } catch (IOException e) { 23 | throw new RuntimeException(e); 24 | } 25 | return builder; 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/fieldresolver/FieldResolverArgumentExceptionSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.fieldresolver 2 | 3 | import com.intuit.graphql.orchestrator.resolverdirective.ResolverArgumentDefinition 4 | import com.intuit.graphql.orchestrator.schema.transform.FieldResolverContext 5 | import spock.lang.Specification 6 | 7 | class FieldResolverArgumentExceptionSpec extends Specification { 8 | 9 | def "FieldResolverArgumentException gives correct error message" () { 10 | given: 11 | String expectedMessage = "testMessage resolverArgumentName=null, resolverArgumentValue=null fieldName=null, parentTypeName=null, resolverDirectiveDefinition=null, serviceNameSpace=null" 12 | ResolverArgumentDefinition mockResolverArgumentDefinition = Mock(ResolverArgumentDefinition.class) 13 | FieldResolverContext mockFieldResolverContext = Mock(FieldResolverContext.class) 14 | FieldResolverArgumentException fieldResolverArgumentException = new FieldResolverArgumentException("testMessage", mockResolverArgumentDefinition, mockFieldResolverContext) 15 | 16 | expect: 17 | fieldResolverArgumentException.message == expectedMessage 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/utils/ExecutionPathUtils.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils; 2 | 3 | import graphql.GraphQLError; 4 | import graphql.execution.ResultPath; 5 | import java.util.Objects; 6 | import org.apache.commons.collections4.CollectionUtils; 7 | import org.apache.commons.lang3.StringUtils; 8 | 9 | public class ExecutionPathUtils { 10 | 11 | private ExecutionPathUtils(){} 12 | 13 | public static boolean graphQLErrorPathStartsWith(GraphQLError graphQLError, ResultPath aliasedResolverExecutionPath) { 14 | Objects.requireNonNull(graphQLError); 15 | Objects.requireNonNull(aliasedResolverExecutionPath); 16 | 17 | if (CollectionUtils.isEmpty(graphQLError.getPath())) { 18 | return false; 19 | } 20 | ResultPath graphQLErrorExecutionPath = ResultPath.fromList(graphQLError.getPath()); 21 | return pathStartsWith(graphQLErrorExecutionPath, aliasedResolverExecutionPath); 22 | } 23 | 24 | public static boolean pathStartsWith(ResultPath pathToTest, ResultPath startPath) { 25 | Objects.requireNonNull(pathToTest); 26 | Objects.requireNonNull(startPath); 27 | 28 | return StringUtils.startsWith(pathToTest.toString(), startPath.toString()); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/xtext/FieldContext.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.xtext; 2 | 3 | import graphql.schema.FieldCoordinates; 4 | import java.util.Objects; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | public class FieldContext { 9 | 10 | private final String parentType; 11 | private final String fieldName; 12 | private final FieldCoordinates fieldCoordinates; 13 | 14 | public FieldContext(String parentType, String fieldName) { 15 | this.parentType = parentType; 16 | this.fieldName = fieldName; 17 | this.fieldCoordinates = FieldCoordinates.coordinates(parentType, fieldName); 18 | } 19 | 20 | @Override 21 | public boolean equals(Object o) { 22 | if (this == o) { 23 | return true; 24 | } 25 | if (o == null || getClass() != o.getClass()) { 26 | return false; 27 | } 28 | FieldContext that = (FieldContext) o; 29 | return Objects.equals(parentType, that.parentType) && 30 | Objects.equals(fieldName, that.fieldName); 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | return Objects.hash(parentType, fieldName); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return parentType + ":" + fieldName; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/resolverdirective/ResolverArgumentPrematureLeafType.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective; 2 | 3 | import com.intuit.graphql.orchestrator.xtext.FieldContext; 4 | 5 | /** 6 | * Exception will occur when parsing the field in a resolver directive field and a ScalarTypeDefinition or 7 | * EnumTypeDefinition is found in any level that is not the leaf node. Scalar types and Enum types are considered 8 | * terminal types and do not have any object nodes underneath them. 9 | * 10 | *

11 | * Example 12 | *

13 | *
14 |  *   {@code @resolver(field: "a.b.c")}
15 |  * 
16 | * 17 | * An exception should be thrown if a ScalarTypeDefinition or EnumTypeDefinition is found in "a", or "b". 18 | */ 19 | public class ResolverArgumentPrematureLeafType extends ResolverDirectiveException { 20 | 21 | private static final String MSG = "Resolver argument '%s' in '%s': Premature %s found in field '%s'."; 22 | 23 | public ResolverArgumentPrematureLeafType(final String argumentName, final String typeName, 24 | final FieldContext rootContext, 25 | String field) { 26 | super(String.format(MSG, argumentName, rootContext.toString(), typeName, field)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/schema/TypeMetadata.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema; 2 | 3 | import com.intuit.graphql.graphQL.TypeDefinition; 4 | import com.intuit.graphql.orchestrator.schema.transform.FieldResolverContext; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import lombok.Getter; 8 | import lombok.NonNull; 9 | import lombok.RequiredArgsConstructor; 10 | 11 | @RequiredArgsConstructor 12 | @Getter 13 | public class TypeMetadata { 14 | @NonNull private final TypeDefinition typeDefinition; 15 | private final Map fieldResolverContextsByFieldName = new HashMap<>(); 16 | 17 | public void addFieldResolverContext(FieldResolverContext fieldResolverContext) { 18 | String fieldName = fieldResolverContext.getFieldName(); 19 | fieldResolverContextsByFieldName.put(fieldName, fieldResolverContext); 20 | } 21 | 22 | /** 23 | * get the FieldResolverContext for the given field name 24 | * @param fieldName field name 25 | * @return null if not found. Otherwise, the actual FieldResolverContext object 26 | */ 27 | public FieldResolverContext getFieldResolverContext(String fieldName) { 28 | return fieldResolverContextsByFieldName.get(fieldName); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/federation/FieldSetUtils.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.federation; 2 | 3 | import static com.intuit.graphql.orchestrator.utils.GraphQLUtil.parser; 4 | import static java.lang.String.join; 5 | 6 | import graphql.language.Document; 7 | import graphql.language.Field; 8 | import graphql.language.OperationDefinition; 9 | import java.util.Set; 10 | import java.util.stream.Collectors; 11 | import org.apache.commons.lang3.StringUtils; 12 | 13 | public class FieldSetUtils { 14 | 15 | private FieldSetUtils() {} 16 | 17 | public static Document getParsedFieldSet(String fieldSetRawValue) { 18 | return parser.parseDocument(join(StringUtils.SPACE, "{",fieldSetRawValue , "}")); 19 | } 20 | 21 | public static Set toFieldSet(String fieldSetRawValue) { 22 | Document document = getParsedFieldSet(fieldSetRawValue); 23 | OperationDefinition operationDefinition = document.getDefinitionsOfType(OperationDefinition.class).get(0); 24 | return operationDefinition.getSelectionSet().getSelections().stream() 25 | .filter(selection -> selection instanceof Field) // TODO validate that selections are Fields only 26 | .map(selection -> (Field) selection) 27 | .collect(Collectors.toSet()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/schema/transform/UnionAndInterfaceTransformer.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema.transform; 2 | 3 | import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.isInterfaceOrUnionType; 4 | 5 | import com.intuit.graphql.graphQL.FieldDefinition; 6 | import com.intuit.graphql.orchestrator.utils.XtextUtils; 7 | import com.intuit.graphql.orchestrator.xtext.XtextGraph; 8 | import java.util.Objects; 9 | import java.util.Optional; 10 | 11 | public class UnionAndInterfaceTransformer implements Transformer { 12 | 13 | @Override 14 | public XtextGraph transform(XtextGraph xtextGraph) { 15 | 16 | Optional unionOrInterface = XtextUtils 17 | .getAllContentsOfType(FieldDefinition.class, xtextGraph.getXtextResourceSet()) 18 | .filter(this::isUnionOrInterface).findAny(); 19 | 20 | if (unionOrInterface.isPresent()) { 21 | return xtextGraph.transform(builder -> builder.hasInterfaceOrUnion(true)); 22 | } 23 | return xtextGraph; 24 | } 25 | 26 | private boolean isUnionOrInterface(FieldDefinition fieldDefinition) { 27 | return Objects.nonNull(fieldDefinition.getNamedType()) && isInterfaceOrUnionType(fieldDefinition.getNamedType()); 28 | } 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/utils/QueryPathUtils.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils; 2 | 3 | import graphql.language.Field; 4 | import graphql.language.FragmentDefinition; 5 | import graphql.language.Node; 6 | import graphql.util.TraverserContext; 7 | import java.util.ArrayList; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | import org.apache.commons.lang3.StringUtils; 12 | 13 | public class QueryPathUtils { 14 | 15 | private static final String PATH_SEPARATOR = "."; 16 | 17 | private QueryPathUtils(){} 18 | 19 | public static List getNodesAsPathList(TraverserContext context) { 20 | List nodes = new ArrayList<>(context.getParentNodes()); 21 | Collections.reverse(nodes); 22 | nodes.add(context.thisNode()); 23 | List pathList = nodes.stream() 24 | .filter(node -> node instanceof Field || node instanceof FragmentDefinition) 25 | .map(node -> (node instanceof Field) ? ((Field)node).getName() : ((FragmentDefinition)node).getName()) 26 | .collect(Collectors.toList()); 27 | 28 | return pathList; 29 | } 30 | 31 | public static String pathListToFQN(List pathList) { 32 | return StringUtils.join(pathList, PATH_SEPARATOR); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/federation/metadata/KeyDirectiveMetadata.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.federation.metadata; 2 | 3 | import static com.intuit.graphql.orchestrator.federation.FieldSetUtils.toFieldSet; 4 | 5 | import com.intuit.graphql.graphQL.Argument; 6 | import com.intuit.graphql.graphQL.Directive; 7 | import com.intuit.graphql.graphQL.ValueWithVariable; 8 | import graphql.language.Field; 9 | import java.util.Optional; 10 | import java.util.Set; 11 | import lombok.Getter; 12 | import lombok.RequiredArgsConstructor; 13 | 14 | @RequiredArgsConstructor 15 | @Getter 16 | public class KeyDirectiveMetadata { 17 | 18 | private final Set fieldSet; 19 | 20 | public static KeyDirectiveMetadata from(Directive directive) { 21 | Optional optionalArgument = directive.getArguments().stream().findFirst(); 22 | if (!optionalArgument.isPresent()) { 23 | // validation is already being done, this should not happen 24 | throw new IllegalStateException("key directive argument not found."); 25 | } 26 | Argument argument = optionalArgument.get(); 27 | ValueWithVariable valueWithVariable = argument.getValueWithVariable(); 28 | String fieldSetValue = valueWithVariable.getStringValue(); 29 | return new KeyDirectiveMetadata(toFieldSet(fieldSetValue)); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a package using Maven and then publish it to GitHub packages when a release is created 2 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path 3 | 4 | name: GraphQL Orchestrator Java Maven Action 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Set up JDK 11 22 | uses: actions/setup-java@v2 23 | with: 24 | java-version: '11' 25 | distribution: 'adopt' 26 | server-id: ossrh 27 | server-username: MAVEN_USERNAME 28 | server-password: MAVEN_PASSWORD 29 | 30 | - name: Build with Maven 31 | if: ${{ github.ref != 'refs/heads/master' }} 32 | run: mvn -B package --file pom.xml 33 | 34 | - name: Publish Snapshot to OSS Maven Repository 35 | if: ${{ github.ref == 'refs/heads/master' }} 36 | run: mvn -B --batch-mode deploy --file pom.xml 37 | env: 38 | MAVEN_USERNAME: ${{ secrets.OSS_SONATYPE_USERNAME }} 39 | MAVEN_PASSWORD: ${{ secrets.OSS_SONATYPE_PASSWORD }} 40 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/integration/DirectiveValidationSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.integration 2 | 3 | import com.intuit.graphql.orchestrator.schema.SchemaParseException 4 | import com.intuit.graphql.orchestrator.stitching.SchemaStitcher 5 | import helpers.BaseIntegrationTestSpecification 6 | 7 | class DirectiveValidationSpec extends BaseIntegrationTestSpecification { 8 | 9 | def testSchema = """ 10 | type Foo1 { 11 | foo1: String @customDir(reason : "reason `in markdown syntax`") 12 | } 13 | 14 | directive @customDir(reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE 15 | directive @customDir(reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE 16 | """ 17 | 18 | def "SchemaParseException is thrown for duplicate directive definition"() { 19 | given: 20 | testService = createSimpleMockService(testSchema, Collections.emptyMap()) 21 | 22 | when: 23 | SchemaStitcher schemaStitcher = SchemaStitcher.newBuilder() 24 | .service(testService) 25 | .build() 26 | schemaStitcher.stitchGraph() 27 | 28 | then: 29 | final SchemaParseException exception = thrown() 30 | exception.message.contains("Duplicate directives in schema") 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/resolverdirective/ResolverArgumentDefinitionSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective 2 | 3 | import com.intuit.graphql.graphQL.NamedType 4 | import com.intuit.graphql.orchestrator.xtext.GraphQLFactoryDelegate 5 | import spock.lang.Specification 6 | 7 | class ResolverArgumentDefinitionSpec extends Specification { 8 | private String TEST_NAME = "testName" 9 | private String TEST_VALUE = "testValue" 10 | 11 | def "Builder with no Params is accessible"() { 12 | given: 13 | ResolverArgumentDefinition.Builder builder = new ResolverArgumentDefinition.Builder() 14 | expect: 15 | builder instanceof ResolverArgumentDefinition.Builder 16 | } 17 | 18 | def "Builder with arguments sets the values correctly"() { 19 | given: 20 | NamedType namedType = GraphQLFactoryDelegate.createObjectType() 21 | ResolverArgumentDefinition resolverArgumentDefinition = new ResolverArgumentDefinition.Builder() 22 | .name(TEST_NAME) 23 | .value(TEST_VALUE) 24 | .namedType(namedType) 25 | .build() 26 | 27 | expect: 28 | resolverArgumentDefinition.name == TEST_NAME 29 | resolverArgumentDefinition.value == TEST_VALUE 30 | resolverArgumentDefinition.namedType == namedType 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/utils/FederationUtilsSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils 2 | 3 | import spock.lang.Specification 4 | 5 | import static com.intuit.graphql.orchestrator.utils.FederationUtils.getUniqueIdFromFieldSet 6 | 7 | class FederationUtilsSpec extends Specification { 8 | def "can Create Unique Id From Field Set Without Children"() { 9 | given: 10 | String fieldSet = "{ foo bar c1}" 11 | 12 | when: 13 | String id = getUniqueIdFromFieldSet(fieldSet) 14 | 15 | then: 16 | id == "barc1foo" 17 | } 18 | 19 | def "can Create Unique Id From Field Set With Children"() { 20 | given: 21 | String fieldSet = "{ foo bar c1 { d1 d2 d3 { e1 e2}}}" 22 | 23 | when: 24 | String id = getUniqueIdFromFieldSet(fieldSet) 25 | 26 | then: 27 | id == "barc1food1d2d3e1e2" 28 | } 29 | 30 | def "reordered Field Set Result In Same Unique Id"() { 31 | given: 32 | String fieldSet1 = "{ foo bar c1 { d1 d2 d3 { e1 e2 } } }" 33 | String fieldSet2 = "{ bar foo c1 { d2 d1 d3 { e2 e1 } } }" 34 | 35 | when: 36 | String id = getUniqueIdFromFieldSet(fieldSet1) 37 | String id2 = getUniqueIdFromFieldSet(fieldSet2) 38 | 39 | then: 40 | id == "barc1food1d2d3e1e2" 41 | id2 == id 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/ServiceProvider.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator; 2 | 3 | import com.intuit.graphql.orchestrator.batch.QueryExecutor; 4 | import java.util.Collections; 5 | import java.util.Map; 6 | import java.util.Set; 7 | 8 | /** 9 | * Represents a Service Provider. 10 | */ 11 | public interface ServiceProvider extends QueryExecutor { 12 | 13 | /** 14 | * The namespace needs to be unique per provider. This will be appended to each type of the service in the overall 15 | * schema. This also helps keep the provider's types separate from the other providers and helps in type conflict 16 | * resolution. 17 | * 18 | * @return A unique identifier for the service provider. 19 | */ 20 | String getNameSpace(); 21 | 22 | /** 23 | * This will represent your graphql schema, you can keep it in single file as well as multiple files. 24 | * 25 | * @return The graphql schema files. 26 | */ 27 | Map sdlFiles(); 28 | 29 | default Set domainTypes() { 30 | return Collections.emptySet(); 31 | } 32 | 33 | default ServiceType getSeviceType() { 34 | return ServiceType.GRAPHQL; 35 | } 36 | 37 | default boolean isFederationProvider() { return getSeviceType() == ServiceType.FEDERATION_SUBGRAPH; } 38 | 39 | enum ServiceType { 40 | FEDERATION_SUBGRAPH, 41 | GRAPHQL, 42 | REST 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/batch/QueryResponseModifierSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.batch 2 | 3 | import graphql.execution.DataFetcherResult 4 | import spock.lang.Specification 5 | 6 | class QueryResponseModifierSpec extends Specification { 7 | 8 | def "default Response Modifier Returns Data Fetcher Result"() { 9 | given: 10 | final HashMap queryResponse = new HashMap<>() 11 | final HashMap internalData = new HashMap<>() 12 | final ArrayList listOfErrors = new ArrayList<>() 13 | 14 | listOfErrors.add(new HashMap<>()) 15 | internalData.put("field", "value") 16 | queryResponse.put("data", internalData) 17 | queryResponse.put("errors", listOfErrors) 18 | 19 | when: 20 | final DataFetcherResult> result = new DefaultQueryResponseModifier().modify(queryResponse) 21 | 22 | then: 23 | result.getData().get("field") == "value" 24 | result.getErrors().size() == 1 25 | } 26 | 27 | def "default Response Modifier Returns Default Collections"() { 28 | when: 29 | final DataFetcherResult> result = new DefaultQueryResponseModifier().modify(new HashMap<>()) 30 | 31 | then: 32 | result.getData() != null 33 | result.getErrors() != null 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/fieldresolver/FieldResolverException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.fieldresolver; 2 | 3 | import com.intuit.graphql.orchestrator.resolverdirective.ResolverDirectiveException; 4 | import com.intuit.graphql.orchestrator.schema.transform.FieldResolverContext; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | public class FieldResolverException extends ResolverDirectiveException { 9 | 10 | private final FieldResolverContext fieldResolverContext; 11 | 12 | public FieldResolverException(String message, FieldResolverContext fieldResolverContext) { 13 | super(formatMessage(message, fieldResolverContext)); 14 | this.fieldResolverContext = fieldResolverContext; 15 | } 16 | 17 | private static String formatMessage(String originalMessage, FieldResolverContext fieldResolverContext) { 18 | String errorMessage = originalMessage + " fieldName=%s, " 19 | + " parentTypeName=%s, " 20 | + " resolverDirectiveDefinition=%s," 21 | + " serviceNameSpace=%s"; 22 | 23 | return String.format(errorMessage, 24 | fieldResolverContext.getFieldName(), 25 | fieldResolverContext.getParentTypename(), 26 | fieldResolverContext.getResolverDirectiveDefinition(), 27 | fieldResolverContext.getServiceNamespace()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/resources/top_level/books-and-pets/mock-responses/get-petById-1to3-with-error-no-paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "message": "Exception while fetching data (/pet_0) : Hello Error!", 5 | "locations": [ 6 | { 7 | "line": 5, 8 | "column": 5 9 | } 10 | ], 11 | "path": null, 12 | "extensions": { 13 | "classification": "DataFetchingException" 14 | } 15 | }, 16 | { 17 | "message": "Exception while fetching data (/pet_1/age) : Null Pointer Exception", 18 | "locations": [ 19 | { 20 | "line": 8, 21 | "column": 5 22 | } 23 | ], 24 | "path": [], 25 | "extensions": { 26 | "classification": "DataFetchingException" 27 | } 28 | }, 29 | { 30 | "message": "Exception while fetching data (/pet_2) : Hello Error!", 31 | "locations": [ 32 | { 33 | "line": 8, 34 | "column": 5 35 | } 36 | ], 37 | "path": null, 38 | "extensions": { 39 | "classification": "DataFetchingException" 40 | } 41 | } 42 | ], 43 | "data": { 44 | "pet_0": null, 45 | "pet_1" : { 46 | "id": "pet-2", 47 | "name": "Milo", 48 | "age": null, 49 | "weight": 20, 50 | "purebred": false, 51 | "type": "RABBIT" 52 | }, 53 | "pet_2": null 54 | } 55 | } -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/utils/FieldReferenceUtilSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils 2 | 3 | import spock.lang.Specification 4 | 5 | class FieldReferenceUtilSpec extends Specification { 6 | 7 | def "get All Field Reference from Emty String returns Empty Set"() { 8 | given: 9 | Set actual = FieldReferenceUtil.getAllFieldReferenceFromString("") 10 | 11 | expect: 12 | actual.isEmpty() 13 | } 14 | 15 | def "get All Field Reference from Null returns Empty Set"() { 16 | given: 17 | Set actual = FieldReferenceUtil.getAllFieldReferenceFromString(null) 18 | 19 | expect: 20 | actual.isEmpty() 21 | } 22 | 23 | def "get All Field Reference from Field Ref returns Extracted Fields"() { 24 | given: 25 | Set actual = FieldReferenceUtil.getAllFieldReferenceFromString('$someFieldRef') 26 | 27 | expect: 28 | actual.size() == 1 29 | actual.contains("someFieldRef") 30 | } 31 | 32 | def "get All Field Reference from Json String returns Extracted Fields"() { 33 | given: 34 | Set actual = FieldReferenceUtil 35 | .getAllFieldReferenceFromString('{ "field": $someFieldRef }') 36 | 37 | expect: 38 | actual.size() == 1 39 | actual.contains("someFieldRef") 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/fieldresolver/FieldResolverArgumentException.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.fieldresolver; 2 | 3 | import com.intuit.graphql.orchestrator.resolverdirective.ResolverArgumentDefinition; 4 | import com.intuit.graphql.orchestrator.resolverdirective.ResolverDirectiveException; 5 | import com.intuit.graphql.orchestrator.schema.transform.FieldResolverContext; 6 | import lombok.Getter; 7 | 8 | @Getter 9 | public class FieldResolverArgumentException extends FieldResolverException { 10 | 11 | private final FieldResolverContext fieldResolverContext; 12 | 13 | public FieldResolverArgumentException(String message, 14 | ResolverArgumentDefinition resolverArgumentDefinition, 15 | FieldResolverContext fieldResolverContext) { 16 | 17 | super(formatMessage(message, resolverArgumentDefinition), fieldResolverContext); 18 | this.fieldResolverContext = fieldResolverContext; 19 | } 20 | 21 | private static String formatMessage(String originalMessage, ResolverArgumentDefinition resolverArgumentDefinition) { 22 | String errorMessage = originalMessage + " resolverArgumentName=%s, resolverArgumentValue=%s"; 23 | 24 | return String.format(errorMessage, resolverArgumentDefinition.getName(), resolverArgumentDefinition.getNamedType()); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/utils/FieldReferenceUtil.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils; 2 | 3 | import java.util.Collections; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | import org.apache.commons.lang3.StringUtils; 9 | 10 | public class FieldReferenceUtil { 11 | 12 | private static final Pattern pattern = Pattern.compile("\\$\\w+"); // matches a-z A-Z _ 0-9 13 | 14 | private FieldReferenceUtil() {} 15 | 16 | /** 17 | * extracts field references from the given string. a field reference 18 | * started with the dollar($) sign and followed by a valid fieldName. 19 | * 20 | * @param inputString string to extract field references from 21 | * @return set of field references 22 | */ 23 | public static Set getAllFieldReferenceFromString(String inputString) { 24 | if (StringUtils.isEmpty(inputString)) { 25 | return Collections.emptySet(); 26 | } 27 | 28 | Set output = new HashSet<>(); 29 | Matcher matcher = pattern.matcher(inputString); 30 | while (matcher.find()) { 31 | String fieldRef = extractVariable(matcher.group()); 32 | output.add(fieldRef); 33 | } 34 | return output; 35 | } 36 | 37 | private static String extractVariable(String string) { 38 | return StringUtils.removeStart(string, "$"); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/test/resources/top_level/books-and-pets/schema-pets.graphqls: -------------------------------------------------------------------------------- 1 | directive @weigthFormat(role: String) on FIELD_DEFINITION 2 | 3 | directive @directiveOne(i: Int = 5, f: Float = 6.0, s: String = "DefaultString", b: Boolean = true, nullP: String, enumP: SomeEnum = ENUMV2, l: [String] = ["DefaultList1"], o:DirectiveInput = { p1: "DefaultP1"}) on FIELD_DEFINITION 4 | 5 | input DirectiveInput { 6 | p1: String 7 | p2: Boolean 8 | } 9 | enum SomeEnum { ENUMV1, ENUMV2 } 10 | 11 | 12 | directive @merge(if: Boolean = true) on FIELD 13 | 14 | type Query { 15 | pets: [Pet] 16 | pet(id: ID!): Pet 17 | } 18 | 19 | type Mutation { 20 | addPet(pet: InputPet!): Pet 21 | } 22 | 23 | type Pet{ 24 | id: ID! 25 | name: String! 26 | age: Int @weigthFormat(role: null) @directiveOne(i: 1 f: 2.0 s: "Yeah" b: false nullP: null enumP: ENUMV1 l: ["s1" "s2"] o: { p1: "Hello"}) 27 | # age: Int @weigthFormat(role: null) 28 | weight: Weight 29 | purebred: Boolean 30 | type: AnimalType @deprecated(reason : "reason `in markdown syntax`") 31 | } 32 | 33 | input InputPet{ 34 | id: ID! 35 | name: String! 36 | age: Int 37 | weight: Weight 38 | purebred: Boolean 39 | tag: String 40 | } 41 | 42 | enum AnimalType { 43 | DOG 44 | CAT 45 | RABBIT 46 | } 47 | 48 | scalar Weight 49 | 50 | directive @deprecated(reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE 51 | 52 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/batch/DefaultBatchResultTransformer.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.batch; 2 | 3 | import static com.intuit.graphql.orchestrator.utils.GraphQLUtil.getErrors; 4 | 5 | import graphql.GraphQLError; 6 | import graphql.execution.DataFetcherResult; 7 | import graphql.language.Field; 8 | import graphql.schema.DataFetchingEnvironment; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.stream.Collectors; 12 | 13 | public class DefaultBatchResultTransformer implements BatchResultTransformer { 14 | 15 | //since top level, expect one key per environment 16 | @Override 17 | public List> toBatchResult( 18 | final DataFetcherResult> result, 19 | final List keys) { 20 | 21 | return keys.stream() 22 | .map(key -> toSingleResult(result, key)) 23 | .collect(Collectors.toList()); 24 | 25 | } 26 | 27 | public static DataFetcherResult toSingleResult(DataFetcherResult> result, 28 | DataFetchingEnvironment environment) { 29 | final Field field = environment.getField(); 30 | Object data = result.getData().get(field.getAlias() != null ? field.getAlias() : field.getName()); 31 | List errors = getErrors(result, field); 32 | return DataFetcherResult.newResult().data(data).errors(errors).build(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/intuit/graphql/orchestrator/MutablePetsService.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import graphql.ExecutionInput; 5 | import graphql.GraphQLContext; 6 | import graphql.language.Document; 7 | import graphql.language.Field; 8 | import graphql.language.OperationDefinition; 9 | import java.util.Map; 10 | import java.util.concurrent.CompletableFuture; 11 | 12 | public class MutablePetsService implements ServiceProvider { 13 | 14 | @Override 15 | public String getNameSpace() { 16 | return "PETS"; 17 | } 18 | 19 | @Override 20 | public Map sdlFiles() { 21 | return TestHelper.getFileMapFromList("top_level/books-and-pets/schema-pets.graphqls"); 22 | } 23 | 24 | @Override 25 | public CompletableFuture> query(ExecutionInput executionInput, 26 | GraphQLContext context) { 27 | 28 | Document document = (Document) executionInput.getRoot(); 29 | OperationDefinition opDep = (OperationDefinition) document.getDefinitions().get(0); 30 | Field field = (Field) opDep.getSelectionSet().getSelections().get(0); 31 | String queryFieldName = field.getName(); 32 | 33 | Map newpet = (Map) executionInput.getVariables().get("newpet"); 34 | return CompletableFuture 35 | .completedFuture(ImmutableMap.of("data", ImmutableMap.of(queryFieldName, newpet))); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/resolverdirective/ResolverArgumentFieldNotInSchema.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective; 2 | 3 | import com.intuit.graphql.orchestrator.xtext.FieldContext; 4 | 5 | /** 6 | * Exception will occur if the requested argument input type (at any recursive level in the input type) does not exist 7 | * in the schema. 8 | * 9 | * Example: 10 | * 11 | *
12 |  *   {@code
13 |  *
14 |  *    input MyInputType {
15 |  *      a: Int
16 |  *      b: Int
17 |  *      c: NestedType
18 |  *    }
19 |  *
20 |  *    input NestedType {
21 |  *      d: Int
22 |  *    }
23 |  *
24 |  *    ...
25 |  *    field(arg1: MyInputType @resolver(field: "root.child.leaf")): String
26 |  *
27 |  *   }
28 |  * 
29 | * 30 | * For the above {@code MyInputType} to be valid, ALL fields found in MyInputType found in "root.child.leaf" should 31 | * exist in the schema. 32 | */ 33 | public class ResolverArgumentFieldNotInSchema extends ResolverDirectiveException { 34 | 35 | private static final String MSG = "Resolver argument '%s' in '%s': field '%s' in InputType '%s' does not exist in schema."; 36 | 37 | public ResolverArgumentFieldNotInSchema(final String argumentName, final FieldContext rootContext, 38 | final FieldContext fieldContext) { 39 | super(String 40 | .format(MSG, argumentName, rootContext.toString(), fieldContext.getFieldName(), fieldContext.getParentType())); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Open source projects are “living.” Contributions in the form of issues and pull requests are welcomed and encouraged. When you contribute, you explicitly say you are part of the community and abide by its Code of Conduct. 2 | 3 | # The Code 4 | 5 | At Intuit, we foster a kind, respectful, harassment-free cooperative community. Our open source community works to: 6 | 7 | - Be kind and respectful; 8 | - Act as a global community; 9 | - Conduct ourselves professionally. 10 | 11 | As members of this community, we will not tolerate behaviors including, but not limited to: 12 | 13 | - Violent threats or language; 14 | - Discriminatory or derogatory jokes or language; 15 | - Public or private harassment of any kind; 16 | - Other conduct considered inappropriate in a professional setting. 17 | 18 | ## Reporting Concerns 19 | 20 | If you see someone violating the Code of Conduct please email TechOpenSource@intuit.com 21 | 22 | ## Scope 23 | 24 | This code of conduct applies to: 25 | 26 | All repos and communities for Intuit-managed projects, whether or not the text is included in a Intuit-managed project’s repository; 27 | 28 | Individuals or teams representing projects in official capacity, such as via official social media channels or at in-person meetups. 29 | 30 | ## Attribution 31 | 32 | This Code of Conduct is partly inspired by and based on those of Amazon, CocoaPods, GitHub, Microsoft, thoughtbot, and on the Contributor Covenant version 1.4.1. -------------------------------------------------------------------------------- /src/test/java/com/intuit/graphql/orchestrator/testhelpers/UnifiedXtextGraphBuilder.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.testhelpers; 2 | 3 | import com.intuit.graphql.graphQL.ObjectTypeDefinition; 4 | import com.intuit.graphql.orchestrator.ServiceProvider; 5 | import com.intuit.graphql.orchestrator.schema.Operation; 6 | import com.intuit.graphql.orchestrator.utils.XtextUtils; 7 | import com.intuit.graphql.orchestrator.xtext.UnifiedXtextGraph; 8 | import com.intuit.graphql.orchestrator.xtext.XtextResourceSetBuilder; 9 | import org.eclipse.xtext.resource.XtextResourceSet; 10 | 11 | import java.util.EnumMap; 12 | import java.util.Map; 13 | 14 | public class UnifiedXtextGraphBuilder { 15 | 16 | public static UnifiedXtextGraph build(ServiceProvider serviceProvider) { 17 | XtextResourceSet xtextResourceSet = XtextResourceSetBuilder.newBuilder() 18 | .files(serviceProvider.sdlFiles()) 19 | .isFederatedResourceSet(serviceProvider.isFederationProvider()) 20 | .build(); 21 | 22 | final Map operationMap = new EnumMap<>(Operation.class); 23 | for (Operation operation : Operation.values()) { 24 | XtextUtils.findOperationType(operation, xtextResourceSet) 25 | .ifPresent(operationType -> operationMap.put(operation, operationType)); 26 | } 27 | 28 | return UnifiedXtextGraph.newBuilder() 29 | .operationMap(operationMap).build(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/schema/ServiceMetadata.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema; 2 | 3 | import com.intuit.graphql.orchestrator.ServiceProvider; 4 | import com.intuit.graphql.orchestrator.federation.metadata.FederationMetadata; 5 | import com.intuit.graphql.orchestrator.metadata.RenamedMetadata; 6 | import com.intuit.graphql.orchestrator.schema.transform.FieldResolverContext; 7 | import graphql.schema.FieldCoordinates; 8 | 9 | public interface ServiceMetadata { 10 | 11 | /** 12 | * Check if the given typeName exists in provider's schema. 13 | * 14 | * @param typeName typeName to check 15 | * @return true or false 16 | */ 17 | boolean hasType(String typeName); 18 | 19 | /** 20 | * Check if the given provider's schema (contains unions/interfaces and) requires typename to be injected in its 21 | * queries 22 | * 23 | * @return true or false 24 | */ 25 | boolean requiresTypenameInjection(); 26 | 27 | boolean hasFieldResolverDirective(); 28 | 29 | ServiceProvider getServiceProvider(); 30 | 31 | FieldResolverContext getFieldResolverContext(FieldCoordinates fieldCoordinates); 32 | 33 | boolean isOwnedByEntityExtension(FieldCoordinates fieldCoordinates); 34 | 35 | boolean isFederationService(); 36 | 37 | boolean isEntity(String typename); 38 | 39 | FederationMetadata getFederationServiceMetadata(); 40 | 41 | boolean shouldModifyDownStreamQuery(); 42 | 43 | RenamedMetadata getRenamedMetadata(); 44 | } 45 | -------------------------------------------------------------------------------- /src/test/resources/top_level/books-and-pets/mock-responses/get-petById-1to3-with-error.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "message": "Exception while fetching data (/pet_0) : Hello Error!", 5 | "locations": [ 6 | { 7 | "line": 5, 8 | "column": 5 9 | } 10 | ], 11 | "path": [ 12 | "pet_0" 13 | ], 14 | "extensions": { 15 | "classification": "DataFetchingException" 16 | } 17 | }, 18 | { 19 | "message": "Exception while fetching data (/pet_1/age) : Null Pointer Exception", 20 | "locations": [ 21 | { 22 | "line": 8, 23 | "column": 5 24 | } 25 | ], 26 | "path": [ 27 | "pet_1", 28 | "age" 29 | ], 30 | "extensions": { 31 | "classification": "DataFetchingException" 32 | } 33 | }, 34 | { 35 | "message": "Exception while fetching data (/pet_2) : Hello Error!", 36 | "locations": [ 37 | { 38 | "line": 8, 39 | "column": 5 40 | } 41 | ], 42 | "path": [ 43 | "pet_2" 44 | ], 45 | "extensions": { 46 | "classification": "DataFetchingException" 47 | } 48 | } 49 | ], 50 | "data": { 51 | "pet_0": null, 52 | "pet_1" : { 53 | "id": "pet-2", 54 | "name": "Milo", 55 | "age": null, 56 | "weight": 20, 57 | "purebred": false, 58 | "type": "RABBIT" 59 | }, 60 | "pet_2": null 61 | } 62 | } -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/datafetcher/XtextResolverArgumentSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.datafetcher 2 | 3 | import spock.lang.Specification 4 | 5 | class XtextResolverArgumentSpec extends Specification { 6 | 7 | def "converts Resolver Argument Fields To Queries"() { 8 | given: 9 | String resolverArgumentField = "query.consumer.fieldA" 10 | // 11 | // XtextResolverArgument result = XtextResolverArgument.newBuilder() 12 | // .resolverArgumentField(resolverArgumentField) 13 | // .build() 14 | // 15 | // assertThat(printAstCompact(result.getPreparedQuery())).isEqualTo("query {consumer {fieldA}}") 16 | } 17 | 18 | def "should Throw Invalid Field Reference Exceptions"() { 19 | given: 20 | String[] resolverArgumentFieldTestCases = [ 21 | "", 22 | " ", 23 | "?.?", 24 | "query", 25 | "..query", 26 | "query..", 27 | "q.a.bc..", 28 | "q..a", 29 | "consumer.finance" 30 | ] 31 | 32 | // for (final String testCase : resolverArgumentFieldTestCases) { 33 | // try { 34 | // XtextResolverArgument.newBuilder().resolverArgumentField(testCase).build() 35 | // } catch (NotAValidFieldReference notAValidFieldReference) { 36 | // continue 37 | // } 38 | // 39 | // fail("Expected NotAValidFieldReference, but passed: " + testCase) 40 | // } 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/VirtualOrchestratorProvider.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator; 2 | 3 | import graphql.ExecutionInput; 4 | import graphql.GraphQLContext; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.concurrent.CompletableFuture; 8 | 9 | public class VirtualOrchestratorProvider implements ServiceProvider { 10 | 11 | public static final String ORCHESTRATOR = "ORCHESTRATOR"; 12 | public static final String FIELD_NAME = "_namespace"; 13 | public static final String SDL = String.format("type Query { %s : String }", FIELD_NAME); 14 | public static final String FILE_NAME = String.format("%s.graphqls", ORCHESTRATOR); 15 | public static final ServiceProvider INSTANCE = new VirtualOrchestratorProvider(); 16 | 17 | private VirtualOrchestratorProvider(){} 18 | 19 | @Override 20 | public String getNameSpace() { 21 | return ORCHESTRATOR; 22 | } 23 | 24 | @Override 25 | public Map sdlFiles() { 26 | return createData(FILE_NAME, SDL); 27 | } 28 | 29 | @Override 30 | public CompletableFuture> query(final ExecutionInput executionInput, 31 | final GraphQLContext context) { 32 | return CompletableFuture.completedFuture(createData("data",createData(FIELD_NAME, ORCHESTRATOR))); 33 | } 34 | 35 | private static Map createData(String key, T value) { 36 | final Map dataMap = new HashMap(); 37 | dataMap.put(key, value); 38 | return dataMap; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/xtext/XtextGraphBuilder.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.xtext; 2 | 3 | import com.intuit.graphql.graphQL.ObjectTypeDefinition; 4 | import com.intuit.graphql.orchestrator.ServiceProvider; 5 | import com.intuit.graphql.orchestrator.schema.Operation; 6 | import com.intuit.graphql.orchestrator.utils.XtextUtils; 7 | 8 | import java.io.IOException; 9 | import java.nio.file.Files; 10 | import java.nio.file.Paths; 11 | import java.util.EnumMap; 12 | import java.util.Map; 13 | 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.apache.commons.io.IOUtils; 16 | import org.eclipse.xtext.resource.XtextResourceSet; 17 | 18 | public class XtextGraphBuilder { 19 | 20 | public static XtextGraph build(ServiceProvider serviceProvider) { 21 | XtextResourceSet xtextResourceSet = XtextResourceSetBuilder.newBuilder() 22 | .files(serviceProvider.sdlFiles()) 23 | .isFederatedResourceSet(serviceProvider.isFederationProvider()) 24 | .build(); 25 | 26 | final Map operationMap = new EnumMap<>(Operation.class); 27 | for (Operation operation : Operation.values()) { 28 | XtextUtils.findOperationType(operation, xtextResourceSet) 29 | .ifPresent(operationType -> operationMap.put(operation, operationType)); 30 | } 31 | 32 | return XtextGraph.newBuilder().xtextResourceSet(xtextResourceSet) 33 | .serviceProvider(serviceProvider) 34 | .operationMap(operationMap).build(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/utils/DescriptionUtils.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | public class DescriptionUtils { 6 | 7 | public static String attachNamespace(String namespace, String description) { 8 | StringBuilder stringBuilder = new StringBuilder(); 9 | stringBuilder.append(String.format("[%s]", StringUtils.trim(namespace))); 10 | if (StringUtils.isNotEmpty(description)) { 11 | stringBuilder.append(StringUtils.SPACE); 12 | stringBuilder.append(StringUtils.trim(description)); 13 | } 14 | return stringBuilder.toString(); 15 | } 16 | 17 | public static String mergeDescriptions(String firstDescription, String secondDescription) { 18 | String desc1 = StringUtils.substringAfter(firstDescription, "]"); 19 | String desc2 = StringUtils.substringAfter(secondDescription, "]"); 20 | String ns1 = StringUtils.substringBetween(firstDescription, "[", "]"); 21 | String ns2 = StringUtils.substringBetween(secondDescription, "[", "]"); 22 | return attachNamespace(join(ns1, ns2), join(desc1, desc2)); 23 | } 24 | 25 | private static String join(String s1, String s2){ 26 | StringBuilder stringBuilder = new StringBuilder(); 27 | stringBuilder.append(StringUtils.defaultString(s1)); 28 | if(!StringUtils.isAnyBlank(s1,s2)){ 29 | stringBuilder.append(','); 30 | } 31 | stringBuilder.append(StringUtils.defaultString(s2)); 32 | return stringBuilder.toString(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/intuit/graphql/orchestrator/PersonService.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import graphql.ExecutionInput; 5 | import graphql.GraphQLContext; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.concurrent.CompletableFuture; 9 | 10 | public class PersonService implements ServiceProvider { 11 | 12 | private Map person; 13 | 14 | PersonService() { 15 | this.person = createPerson(); 16 | } 17 | 18 | private Map createPerson() { 19 | Map addressMap = new HashMap<>(); 20 | addressMap.put("id","address-1"); 21 | addressMap.put("street", "Lombok Street"); 22 | addressMap.put("city", "San Diego"); 23 | addressMap.put("zip", "12345"); 24 | addressMap.put("state", "CA"); 25 | addressMap.put("country", "United States"); 26 | return ImmutableMap.of("id", "person-1", "name", "Kevin Whitney", "address", addressMap); 27 | } 28 | 29 | @Override 30 | public String getNameSpace() { 31 | return "PERSON"; 32 | } 33 | 34 | @Override 35 | public Map sdlFiles() { 36 | return TestHelper.getFileMapFromList("nested/books-pets-person/schema-person.graphqls"); 37 | } 38 | 39 | @Override 40 | public CompletableFuture> query(ExecutionInput executionInput, 41 | GraphQLContext context) { 42 | return CompletableFuture.completedFuture(ImmutableMap.of("data", ImmutableMap.of("person", this.person))); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/authorization/ValidateMultipleDirectivesCoexist.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.authorization; 2 | 3 | import com.intuit.graphql.graphQL.Directive; 4 | import com.intuit.graphql.orchestrator.stitching.InvalidDirectivePairingException; 5 | import org.apache.commons.collections4.CollectionUtils; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | 11 | public class ValidateMultipleDirectivesCoexist { 12 | 13 | public ValidateMultipleDirectivesCoexist() { 14 | } 15 | 16 | public void validate(List directives) { 17 | List directiveNames = directives.stream() 18 | .map(d -> d.getDefinition().getName()) 19 | .collect(Collectors.toList()); 20 | 21 | if (CollectionUtils.containsAll(directiveNames, Arrays.asList("resolver", "external"))) { 22 | throw new InvalidDirectivePairingException(Arrays.asList("resolver", "external")); 23 | } 24 | 25 | if (CollectionUtils.containsAll(directiveNames, Arrays.asList("resolver", "provides"))) { 26 | throw new InvalidDirectivePairingException(Arrays.asList("resolver", "external")); 27 | } 28 | 29 | if (CollectionUtils.containsAll(directiveNames, Arrays.asList("resolver", "requires"))) { 30 | throw new InvalidDirectivePairingException(Arrays.asList("resolver", "external")); 31 | } 32 | 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release to maven-central 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | releaseversion: 6 | description: 'Release version' 7 | required: false 8 | default: 'X.Y.Z' 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - run: echo "Will release to central maven" 14 | 15 | - uses: actions/checkout@v2 16 | with: 17 | token: ${{ secrets.REPO_GIT_TOKEN }} 18 | 19 | - name: Set up Maven Central Repository 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 11 23 | server-id: ossrh 24 | server-username: MAVEN_USERNAME 25 | server-password: MAVEN_PASSWORD 26 | gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} 27 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 28 | 29 | - name: Configure Git User 30 | run: | 31 | git config user.email "actions@github.com" 32 | git config user.name "GitHub Actions" 33 | # - name: Set projects Maven version to GitHub Action GUI set version 34 | # run: mvn versions:set "-DnewVersion=${{ github.event.inputs.releaseversion }}" 35 | 36 | - name: Publish package 37 | run: mvn --batch-mode release:prepare release:perform -P release -DskipTests=true 38 | env: 39 | MAVEN_USERNAME: ${{ secrets.OSS_SONATYPE_USERNAME }} 40 | MAVEN_PASSWORD: ${{ secrets.OSS_SONATYPE_PASSWORD }} 41 | MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} 42 | -------------------------------------------------------------------------------- /src/test/java/com/intuit/graphql/orchestrator/NestedBooksService.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import graphql.ExecutionInput; 5 | import graphql.GraphQLContext; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.concurrent.CompletableFuture; 9 | 10 | public class NestedBooksService implements ServiceProvider { 11 | 12 | private Map book; 13 | 14 | NestedBooksService() { 15 | this.book = createBook(); 16 | } 17 | 18 | private Map createBook() { 19 | Map b1 = new HashMap<>(); 20 | b1.put("id", "book-1"); 21 | b1.put("name", "GraphQL Advanced Stitching"); 22 | b1.put("pageCount", "2000"); 23 | b1.put("author", ImmutableMap.of( 24 | "id","author-1", 25 | "lastName","AuthorOne" 26 | )); 27 | return b1; 28 | } 29 | 30 | @Override 31 | public String getNameSpace() { 32 | return "BOOKS"; 33 | } 34 | 35 | @Override 36 | public Map sdlFiles() { 37 | return TestHelper.getFileMapFromList( 38 | "nested/books-pets-person/schema-books.graphqls", 39 | "nested/books-pets-person/pet-author-link.graphqls" 40 | ); 41 | } 42 | 43 | @Override 44 | public CompletableFuture> query(ExecutionInput executionInput, 45 | GraphQLContext context) { 46 | return CompletableFuture.completedFuture( 47 | ImmutableMap.of("data", ImmutableMap.of("person", ImmutableMap.of("book", this.book)))); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/federation/Federation2PureGraphQLUtil.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.federation; 2 | 3 | import static com.intuit.graphql.orchestrator.utils.FederationUtils.isFederationDirective; 4 | import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.getFieldDefinitions; 5 | 6 | import com.intuit.graphql.graphQL.Directive; 7 | import com.intuit.graphql.graphQL.FieldDefinition; 8 | import com.intuit.graphql.graphQL.TypeDefinition; 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | 12 | public class Federation2PureGraphQLUtil { 13 | 14 | public static void makeAsPureGraphQL(TypeDefinition typeDefinition) { 15 | List directives = typeDefinition.getDirectives(); 16 | removeFederationDirectives(directives); 17 | 18 | List fieldDefinitions = getFieldDefinitions(typeDefinition); 19 | fieldDefinitions.forEach(Federation2PureGraphQLUtil::makeAsPureGraphQL); 20 | } 21 | 22 | private static void makeAsPureGraphQL(FieldDefinition fieldDefinition) { 23 | List directives = fieldDefinition.getDirectives(); 24 | removeFederationDirectives(directives); 25 | } 26 | 27 | private static void removeFederationDirectives(List directives) { 28 | List nonFederationDirectives = 29 | directives.stream() 30 | .filter(directive -> !isFederationDirective(directive)) 31 | .collect(Collectors.toList()); 32 | directives.clear(); 33 | directives.addAll(nonFederationDirectives); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/datafetcher/FieldResolverDirectiveDataFetcherSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.datafetcher 2 | 3 | import com.intuit.graphql.orchestrator.ServiceProvider 4 | import com.intuit.graphql.orchestrator.schema.transform.FieldResolverContext 5 | import com.intuit.graphql.orchestrator.xtext.DataFetcherContext 6 | import spock.lang.Specification 7 | 8 | class FieldResolverDirectiveDataFetcherSpec extends Specification { 9 | 10 | FieldResolverDirectiveDataFetcher dataFetcher 11 | 12 | def setup() { 13 | FieldResolverContext fieldResolverContext = Mock(FieldResolverContext.class) 14 | dataFetcher = new FieldResolverDirectiveDataFetcher(fieldResolverContext, "TestNamespace", ServiceProvider.ServiceType.GRAPHQL) 15 | } 16 | 17 | def "returns correct namespace"() { 18 | when: 19 | String actualNamespace = dataFetcher.getNamespace() 20 | 21 | then: 22 | actualNamespace == "TestNamespace" 23 | } 24 | 25 | def "returns correct DataFetcherType"() { 26 | when: 27 | DataFetcherContext.DataFetcherType actualDataFetcherType = dataFetcher.getDataFetcherType() 28 | 29 | then: 30 | actualDataFetcherType == DataFetcherContext.DataFetcherType.RESOLVER_ON_FIELD_DEFINITION 31 | } 32 | 33 | def "returns correct ServiceType"() { 34 | when: 35 | ServiceProvider.ServiceType actualServiceType = dataFetcher.getServiceType() 36 | 37 | then: 38 | actualServiceType == ServiceProvider.ServiceType.GRAPHQL 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/batch/DefaultQueryResponseModifier.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.batch; 2 | 3 | import static java.util.Collections.emptyList; 4 | 5 | import com.intuit.graphql.orchestrator.schema.GraphQLObjects; 6 | import com.intuit.graphql.orchestrator.schema.RawGraphQLError; 7 | import graphql.GraphQLError; 8 | import graphql.execution.DataFetcherResult; 9 | import java.util.Collections; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.Optional; 13 | import java.util.stream.Collectors; 14 | 15 | public class DefaultQueryResponseModifier implements QueryResponseModifier { 16 | 17 | @Override 18 | public DataFetcherResult> modify(final Map queryResponse) { 19 | Map data = Optional.ofNullable(queryResponse.get("data")) 20 | .map(GraphQLObjects::>cast) 21 | .orElse(Collections.emptyMap()); 22 | final List errorsMap = Optional.ofNullable(queryResponse.get("errors")) 23 | .map(GraphQLObjects::>cast) 24 | .orElse(emptyList()); 25 | 26 | List errors = errorsMap.stream() 27 | .map(val -> Optional.ofNullable(val) 28 | .map(GraphQLObjects::>cast) 29 | .orElseThrow(IllegalArgumentException::new)) 30 | .map(RawGraphQLError::new) 31 | .collect(Collectors.toList()); 32 | 33 | return DataFetcherResult.>newResult() 34 | .data(data) 35 | .errors(errors) 36 | .build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/authorization/FieldAuthorization.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.authorization; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | import org.apache.commons.lang3.StringUtils; 5 | 6 | /** 7 | * This interface allows applications using this library to implement field level authorization. An 8 | * application may provide a custom implementation by adding it in the GraphqlQL Context e.g. * 9 | * graphQLContext.put(FieldAuthorization.class, appCustomFieldAuthorizationObject). 10 | * 11 | *

If the implementation of {@link #authorize(FieldAuthorizationEnvironment)} requires an input 12 | * data, here referred to as authData, the {@link #getFutureAuthData()} may be implemented. 13 | * Otherwise, no need to implement {@link #getFutureAuthData()} as this interface provides a default 14 | * implementation. 15 | */ 16 | public interface FieldAuthorization { 17 | 18 | /** 19 | * A hook to get an authData from CompletableFuture. 20 | * 21 | * @return future authorization data which shall be used as an input to 22 | * {@link #authorize(FieldAuthorizationEnvironment)} 23 | */ 24 | default CompletableFuture getFutureAuthData() { 25 | return CompletableFuture.completedFuture(StringUtils.EMPTY); 26 | } 27 | 28 | /** 29 | * interface for the authorization logic. 30 | * 31 | * @param fieldAuthorizationEnvironment data about field requiring authorization 32 | * @return an instance of {@link FieldAuthorizationResult} 33 | */ 34 | FieldAuthorizationResult authorize(FieldAuthorizationEnvironment fieldAuthorizationEnvironment); 35 | } -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/integration/NestedFieldsArgumentsSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.integration 2 | 3 | import com.intuit.graphql.orchestrator.GraphQLOrchestrator 4 | import com.intuit.graphql.orchestrator.ServiceProvider 5 | import com.intuit.graphql.orchestrator.schema.transform.FieldMergeException 6 | import helpers.BaseIntegrationTestSpecification 7 | 8 | class NestedFieldsArgumentsSpec extends BaseIntegrationTestSpecification { 9 | def mockAServiceResponse = [ 10 | data: [ 11 | serviceA: [] 12 | ] 13 | ] 14 | 15 | def mockBServiceResponse = [ 16 | data: [ 17 | serviceB: [] 18 | ] 19 | ] 20 | 21 | def "cannot Build Due To Query Nested Fields Has Mismatched Arguments"() { 22 | def serviceA = createSimpleMockService( "SVCA", "type Query { container : Container } " 23 | + "type Container { serviceA : ServiceA } " 24 | + "type ServiceA { svcAField1 : String }", mockAServiceResponse) 25 | 26 | def serviceB = createSimpleMockService( "SVCB", "type Query { container(in : String) : Container } " 27 | + "type Container { serviceB : ServiceB } " 28 | + "type ServiceB { svcBField1 : String }", mockBServiceResponse) 29 | 30 | 31 | when: 32 | ServiceProvider[] services = [ serviceA, serviceB ] 33 | GraphQLOrchestrator orchestrator = createGraphQLOrchestrator(services) 34 | 35 | then: 36 | thrown(FieldMergeException) 37 | 38 | orchestrator == null 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/integration/NestedFieldsDirectivesSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.integration 2 | 3 | import com.intuit.graphql.orchestrator.GraphQLOrchestrator 4 | import com.intuit.graphql.orchestrator.ServiceProvider 5 | import com.intuit.graphql.orchestrator.schema.transform.FieldMergeException 6 | import helpers.BaseIntegrationTestSpecification 7 | 8 | class NestedFieldsDirectivesSpec extends BaseIntegrationTestSpecification { 9 | 10 | def mockAServiceResponse = [ 11 | data: [ 12 | serviceA: [] 13 | ] 14 | ] 15 | 16 | def mockCServiceResponse = [ 17 | data: [ 18 | serviceC: [] 19 | ] 20 | ] 21 | 22 | 23 | def "cannot Build Due To Query Nested Fields Has Mismatched Directives"() { 24 | def serviceA = createSimpleMockService( "SVCA", "type Query { container : Container } " 25 | + "type Container { serviceA : ServiceA } " 26 | + "type ServiceA { svcAField1 : String }", mockAServiceResponse) 27 | 28 | def serviceC = createSimpleMockService( "SVCC", "type Query { container : Container @deprecated } " 29 | + "type Container { serviceC : ServiceC } " 30 | + "type ServiceC { svcCField1 : String }", mockCServiceResponse) 31 | 32 | 33 | when: 34 | ServiceProvider[] services = [serviceA, serviceC] 35 | GraphQLOrchestrator orchestrator = createGraphQLOrchestrator(services) 36 | 37 | then: 38 | thrown(FieldMergeException) 39 | 40 | orchestrator == null 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/schema/ServiceMetadataImplSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema 2 | 3 | import com.intuit.graphql.orchestrator.ServiceProvider 4 | import com.intuit.graphql.orchestrator.federation.metadata.FederationMetadata 5 | import com.intuit.graphql.orchestrator.metadata.RenamedMetadata 6 | import spock.lang.Specification 7 | 8 | class ServiceMetadataImplSpec extends Specification { 9 | private ServiceProvider serviceProviderMock 10 | private RenamedMetadata renamedMetadataMock 11 | private FederationMetadata federationMetadataMock 12 | ServiceMetadataImpl serviceMetadataImpl 13 | 14 | def setup() { 15 | serviceProviderMock = Mock(ServiceProvider.class) 16 | renamedMetadataMock = Mock(RenamedMetadata.class) 17 | serviceMetadataImpl = ServiceMetadataImpl.newBuilder() 18 | .serviceProvider(serviceProviderMock) 19 | .typeMetadataMap(new HashMap()) 20 | .renamedMetadata(renamedMetadataMock) 21 | .federationMetadata(federationMetadataMock) 22 | .hasFieldResolverDefinition(false) 23 | .hasInterfaceOrUnion(false) 24 | .build() 25 | } 26 | 27 | def "newBuilder with ServiceMetadataImpl arg sets the values correctly"() { 28 | given: 29 | ServiceMetadataImpl testServiceMetadataImpl = ServiceMetadataImpl.newBuilder(serviceMetadataImpl).build() 30 | expect: 31 | !testServiceMetadataImpl.hasFieldResolverDirective() 32 | !testServiceMetadataImpl.isHasInterfaceOrUnion() 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/utils/TypeReferenceUtil.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils; 2 | 3 | import com.intuit.graphql.graphQL.ObjectTypeDefinition; 4 | import com.intuit.graphql.graphQL.TypeDefinition; 5 | import com.intuit.graphql.orchestrator.xtext.XtextGraph; 6 | import java.util.List; 7 | import org.eclipse.emf.ecore.EStructuralFeature.Setting; 8 | import org.eclipse.emf.ecore.util.EcoreUtil.UsageCrossReferencer; 9 | import org.eclipse.xtext.EcoreUtil2; 10 | 11 | public class TypeReferenceUtil { 12 | 13 | public static void updateTypeReferencesInAnXtextGraph( 14 | TypeDefinition targetType, 15 | TypeDefinition replacementType, 16 | XtextGraph xtextGraph) { 17 | List crossReferences = 18 | (List) UsageCrossReferencer.find(targetType, xtextGraph.getXtextResourceSet()); 19 | updateAllCrossReferences(crossReferences, targetType, replacementType); 20 | } 21 | 22 | private static void updateAllCrossReferences( 23 | List crossReferences, TypeDefinition targetType, TypeDefinition replacementType) { 24 | for (Setting setting : crossReferences) { 25 | EcoreUtil2.replace(setting, targetType, replacementType); 26 | } 27 | } 28 | 29 | public static void updateTypeReferencesInObjectType( 30 | TypeDefinition targetType, 31 | TypeDefinition replacementType, 32 | ObjectTypeDefinition objectTypeDefinition) { 33 | 34 | List crossReferences = 35 | (List) UsageCrossReferencer.find(targetType, objectTypeDefinition); 36 | updateAllCrossReferences(crossReferences, targetType, replacementType); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/federation/metadata/FederationMetadataSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.federation.metadata 2 | 3 | import com.intuit.graphql.orchestrator.ServiceProvider 4 | import com.intuit.graphql.orchestrator.TestServiceProvider 5 | import graphql.language.Field 6 | import spock.lang.Specification 7 | 8 | class FederationMetadataSpec extends Specification { 9 | 10 | def "test getBaseServiceProvider of entity extension metadata"() { 11 | given: 12 | ServiceProvider serviceProvider = TestServiceProvider.newBuilder().build() 13 | FederationMetadata federationMetadata = new FederationMetadata(serviceProvider) 14 | FederationMetadata.EntityExtensionMetadata entityExtensionMetadata = FederationMetadata.EntityExtensionMetadata.builder() 15 | .typeName("typeName") 16 | .keyDirectives(new ArrayList()) 17 | .requiredFieldsByFieldName(new HashMap>()) 18 | .federationMetadata(federationMetadata) 19 | .baseEntityMetadata(FederationMetadata.EntityMetadata.builder() 20 | .typeName("typeName") 21 | .keyDirectives(new ArrayList()) 22 | .fields(new HashSet()) 23 | .federationMetadata(federationMetadata) 24 | .build()) 25 | .build(); 26 | ServiceProvider serviceProviderObserved = entityExtensionMetadata.getBaseServiceProvider() 27 | expect: 28 | serviceProviderObserved == serviceProvider 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/schema/TypeMetadataSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema 2 | 3 | import com.intuit.graphql.graphQL.TypeDefinition 4 | import com.intuit.graphql.orchestrator.schema.transform.FieldResolverContext 5 | import spock.lang.Specification 6 | import spock.lang.Subject 7 | 8 | class TypeMetadataSpec extends Specification { 9 | 10 | private static final String TEST_FIELD_NAME = "testField" 11 | 12 | @Subject 13 | TypeMetadata subjectUnderTest 14 | 15 | TypeDefinition typeDefinitionMock 16 | 17 | FieldResolverContext fieldResolverContextMock 18 | 19 | def setup() { 20 | typeDefinitionMock = Mock(TypeDefinition.class) 21 | fieldResolverContextMock = Mock(FieldResolverContext.class) 22 | 23 | subjectUnderTest = new TypeMetadata(typeDefinitionMock) 24 | } 25 | 26 | def "get Field Resolver Context with No Field Resolvers returns Null"() { 27 | given: 28 | subjectUnderTest.addFieldResolverContext(fieldResolverContextMock) 29 | 30 | when: 31 | def actual = subjectUnderTest.getFieldResolverContext(TEST_FIELD_NAME) 32 | 33 | then: 34 | actual == null 35 | } 36 | 37 | def "get Field Resolver Context with Field Resolvers can return Object"() { 38 | given: 39 | fieldResolverContextMock.getFieldName() >> TEST_FIELD_NAME 40 | subjectUnderTest.addFieldResolverContext(fieldResolverContextMock) 41 | 42 | when: 43 | def actual = subjectUnderTest.getFieldResolverContext(TEST_FIELD_NAME) 44 | 45 | then: 46 | actual.is(fieldResolverContextMock) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/datafetcher/AliasablePropertyDataFetcher.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.datafetcher; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import graphql.schema.DataFetcher; 6 | import graphql.schema.DataFetchingEnvironment; 7 | import graphql.schema.PropertyDataFetcher; 8 | import java.util.Map; 9 | 10 | /** 11 | * An {@link AliasablePropertyDataFetcher} that supports returning fields that have been aliased even if the data source 12 | * is a Map. If the source is not a map, just delegates to {@link PropertyDataFetcher} 13 | */ 14 | public class AliasablePropertyDataFetcher implements DataFetcher { 15 | 16 | private final String propertyName; 17 | private final PropertyDataFetcher defaultDataFetcher; 18 | 19 | public AliasablePropertyDataFetcher(final String propertyName) { 20 | this.propertyName = requireNonNull(propertyName); 21 | this.defaultDataFetcher = PropertyDataFetcher.fetching(propertyName); 22 | } 23 | 24 | @Override 25 | public Object get(final DataFetchingEnvironment env) { 26 | final Object source = env.getSource(); 27 | if (source instanceof Map) { 28 | return getValueFromMap((Map) source, env); 29 | } 30 | return defaultDataFetcher.get(env); 31 | 32 | } 33 | 34 | private Object getValueFromMap(Map source, final DataFetchingEnvironment env) { 35 | String alias = env.getField().getAlias(); 36 | 37 | // if an alias was provided then the map should know about the field by its alias 38 | if (alias != null && !alias.isEmpty() && source.containsKey(alias)) { 39 | return source.get(alias); 40 | } 41 | return source.get(propertyName); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/utils/SelectionSetUtil.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils; 2 | 3 | import graphql.language.Field; 4 | import graphql.language.SelectionSet; 5 | import java.util.Optional; 6 | import org.apache.commons.collections4.CollectionUtils; 7 | import org.apache.commons.lang3.StringUtils; 8 | 9 | public class SelectionSetUtil { 10 | 11 | public static final String PATH_DELIMITER = "."; 12 | 13 | public static boolean isEmpty(SelectionSet selectionSet) { 14 | return selectionSet == null || CollectionUtils.isEmpty(selectionSet.getSelections()); 15 | } 16 | 17 | public static Field getFieldWithPath(String path, SelectionSet selectionSet) { 18 | String[] pathTokens = StringUtils.split(path, PATH_DELIMITER); 19 | Field output = null; 20 | for (String pathToken : pathTokens) { 21 | Optional optionalField = getField(pathToken, selectionSet); 22 | if (optionalField.isPresent()) { 23 | output = optionalField.get(); 24 | selectionSet = output.getSelectionSet(); 25 | } else { 26 | throw new IllegalArgumentException(String.format("Invalid Path. field with name '%s' not found ", pathToken)); 27 | } 28 | } 29 | return output; 30 | } 31 | 32 | public static Optional getField(String fieldName, SelectionSet selectionSet) { 33 | return selectionSet.getSelections().stream() 34 | .filter(selection -> selection instanceof Field) 35 | .map(selection -> (Field) selection) 36 | .filter(field -> field.getName().equals(fieldName)) 37 | .findAny(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/datafetcher/ServiceDataFetcher.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.datafetcher; 2 | 3 | import com.intuit.graphql.orchestrator.ServiceProvider.ServiceType; 4 | import com.intuit.graphql.orchestrator.schema.ServiceMetadata; 5 | import com.intuit.graphql.orchestrator.xtext.DataFetcherContext.DataFetcherType; 6 | import graphql.GraphQLContext; 7 | import graphql.schema.DataFetchingEnvironment; 8 | import java.util.concurrent.CompletableFuture; 9 | import lombok.Getter; 10 | 11 | @Getter 12 | public class ServiceDataFetcher implements ServiceAwareDataFetcher { 13 | 14 | /** 15 | * One of Query or Mutation or Subscription 16 | */ 17 | private final ServiceMetadata serviceMetadata; 18 | 19 | 20 | public ServiceDataFetcher(ServiceMetadata serviceMetadata) { 21 | this.serviceMetadata = serviceMetadata; 22 | } 23 | 24 | @Override 25 | public Object get(final DataFetchingEnvironment environment) { 26 | String dfeFieldName = environment.getField().getName(); 27 | GraphQLContext context = environment.getContext(); 28 | context.put(dfeFieldName, this.serviceMetadata); 29 | CompletableFuture r = environment 30 | .getDataLoader(serviceMetadata.getServiceProvider().getNameSpace()) 31 | .load(environment); 32 | return r; 33 | } 34 | 35 | @Override 36 | public String getNamespace() { 37 | return this.serviceMetadata.getServiceProvider().getNameSpace(); 38 | } 39 | 40 | @Override 41 | public DataFetcherType getDataFetcherType() { 42 | return DataFetcherType.SERVICE; 43 | } 44 | 45 | @Override 46 | public ServiceType getServiceType() { 47 | return this.serviceMetadata.getServiceProvider().getSeviceType(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/batch/VariableReferenceExtractor.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.batch; 2 | 3 | import graphql.language.ArrayValue; 4 | import graphql.language.ObjectField; 5 | import graphql.language.ObjectValue; 6 | import graphql.language.Value; 7 | import graphql.language.VariableReference; 8 | import java.util.HashSet; 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | public class VariableReferenceExtractor { 13 | 14 | private final Set variableReferences = new HashSet<>(); 15 | 16 | public Set getVariableReferences() { 17 | return new HashSet<>(variableReferences); 18 | } 19 | 20 | public void captureVariableReferences(List values) { 21 | for (final Value value : values) { 22 | doSwitch(value); 23 | } 24 | } 25 | 26 | private void doSwitch(Value value) { 27 | if (value instanceof ArrayValue) { 28 | handleArrayValue((ArrayValue) value); 29 | } else if (value instanceof ObjectValue) { 30 | handleObjectValue(((ObjectValue) value)); 31 | } else if (value instanceof VariableReference) { 32 | handleVariableReference((VariableReference) value); 33 | } 34 | } 35 | 36 | private void handleVariableReference(VariableReference variableReference) { 37 | variableReferences.add(variableReference); 38 | } 39 | 40 | private void handleArrayValue(ArrayValue arrayValue) { 41 | for (final Value value : arrayValue.getValues()) { 42 | doSwitch(value); 43 | } 44 | } 45 | 46 | private void handleObjectValue(ObjectValue objectValue) { 47 | for (final ObjectField objectField : objectValue.getObjectFields()) { 48 | doSwitch(objectField.getValue()); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/fieldresolver/FieldResolverGraphQLErrorSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.fieldresolver 2 | 3 | import com.intuit.graphql.orchestrator.resolverdirective.ResolverDirectiveDefinition 4 | import graphql.ErrorType 5 | import spock.lang.Specification 6 | 7 | class FieldResolverGraphQLErrorSpec extends Specification { 8 | 9 | private String TEST_ERROR_MESSAGE = "testErrorMessage" 10 | private String TEST_SERVICE_NAME = "testServiceName" 11 | private String TEST_PARENT_TYPE_NAME = "testParentTypeName" 12 | private String TEST_FIELD_NAME = "testFieldName" 13 | 14 | private ResolverDirectiveDefinition mockResolverDirectiveDefinition 15 | 16 | def "fieldResolverGraphQLError error message is correctly formed" () { 17 | given: 18 | mockResolverDirectiveDefinition = Mock(ResolverDirectiveDefinition.class) 19 | String expectedMessage = "testErrorMessage. fieldName=testFieldName, parentTypeName=testParentTypeName, resolverDirectiveDefinition=Mock for type 'ResolverDirectiveDefinition' named 'mockResolverDirectiveDefinition', serviceNameSpace=testServiceName" 20 | FieldResolverGraphQLError fieldResolverGraphQLError = FieldResolverGraphQLError.builder() 21 | .fieldName(TEST_FIELD_NAME) 22 | .parentTypeName(TEST_PARENT_TYPE_NAME) 23 | .resolverDirectiveDefinition(mockResolverDirectiveDefinition) 24 | .serviceNameSpace(TEST_SERVICE_NAME) 25 | .errorMessage(TEST_ERROR_MESSAGE) 26 | .build() 27 | 28 | expect: 29 | fieldResolverGraphQLError.message == expectedMessage 30 | fieldResolverGraphQLError.errorType == ErrorType.ExecutionAborted 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/federation/EntityDataFetcher.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.federation; 2 | 3 | import static com.intuit.graphql.orchestrator.batch.DataLoaderKeyUtil.createDataLoaderKey; 4 | 5 | import com.intuit.graphql.orchestrator.ServiceProvider.ServiceType; 6 | import com.intuit.graphql.orchestrator.datafetcher.ServiceAwareDataFetcher; 7 | import com.intuit.graphql.orchestrator.xtext.DataFetcherContext.DataFetcherType; 8 | import graphql.schema.DataFetchingEnvironment; 9 | import java.util.concurrent.CompletableFuture; 10 | import lombok.RequiredArgsConstructor; 11 | 12 | /** 13 | * This class is used for resolving fields added to an Entity by making an entity fetch request. To 14 | * build the entity fetch query, it uses {@link EntityQuery}. 15 | */ 16 | @RequiredArgsConstructor 17 | public class EntityDataFetcher implements ServiceAwareDataFetcher> { 18 | private final String entityName; 19 | private final String namespace; 20 | private final ServiceType serviceType; 21 | 22 | @Override 23 | public CompletableFuture get(final DataFetchingEnvironment dataFetchingEnvironment) { 24 | String batchLoaderKey = createDataLoaderKey(entityName, dataFetchingEnvironment.getField().getName()); 25 | 26 | return dataFetchingEnvironment 27 | .getDataLoader(batchLoaderKey) 28 | .load(dataFetchingEnvironment); 29 | } 30 | 31 | @Override 32 | public String getNamespace() { 33 | return this.namespace; 34 | } 35 | 36 | @Override 37 | public DataFetcherType getDataFetcherType() { 38 | return DataFetcherType.ENTITY_FETCHER; 39 | } 40 | 41 | @Override 42 | public ServiceType getServiceType() { 43 | return this.serviceType; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/integration/MutationNestedFieldsArgumentsSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.integration 2 | 3 | import com.intuit.graphql.orchestrator.GraphQLOrchestrator 4 | import com.intuit.graphql.orchestrator.ServiceProvider 5 | import com.intuit.graphql.orchestrator.schema.transform.FieldMergeException 6 | import helpers.BaseIntegrationTestSpecification 7 | 8 | class MutationNestedFieldsArgumentsSpec extends BaseIntegrationTestSpecification { 9 | def mockDServiceResponse = [ 10 | data: [ 11 | serviceD: [] 12 | ] 13 | ] 14 | 15 | def mockEServiceResponse = [ 16 | data: [ 17 | serviceD: [] 18 | ] 19 | ] 20 | 21 | def "cannot Build Due To Mutation Nested Fields Has Mismatched Arguments"() { 22 | def serviceD = createSimpleMockService( "SVCD", "type Query { } " 23 | + "type Mutation { container(in : String) : Container } " 24 | + "type Container { serviceD : ServiceD } " 25 | + "type ServiceD { svcDField1 : String }", mockDServiceResponse) 26 | 27 | def serviceE = createSimpleMockService( "SVCE", "type Query { } " 28 | + "type Mutation { container(out : String) : Container } " 29 | + "type Container { serviceE : ServiceE } " 30 | + "type ServiceE { svcEField1 : String }", mockEServiceResponse) 31 | 32 | 33 | given: 34 | ServiceProvider[] services = [ serviceD, serviceE ] 35 | 36 | when: 37 | GraphQLOrchestrator orchestrator = createGraphQLOrchestrator(services) 38 | 39 | then: 40 | thrown(FieldMergeException) 41 | 42 | orchestrator == null 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/schema/Operation.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.schema; 2 | 3 | 4 | import static com.intuit.graphql.orchestrator.xtext.GraphQLFactoryDelegate.createObjectTypeDefinition; 5 | 6 | import com.intuit.graphql.graphQL.ObjectTypeDefinition; 7 | import graphql.schema.GraphQLObjectType; 8 | import java.util.EnumSet; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import org.apache.commons.lang3.StringUtils; 12 | 13 | /** 14 | * The enum Operation. 15 | */ 16 | public enum Operation { 17 | 18 | 19 | QUERY("Query"), 20 | MUTATION("Mutation"), 21 | SUBSCRIPTION("Subscription"); 22 | 23 | private final String name; 24 | 25 | Operation(String name) { 26 | this.name = name; 27 | } 28 | 29 | private static final Map operationMap = new HashMap<>(); 30 | 31 | static { 32 | EnumSet.allOf(Operation.class).forEach(operation -> { 33 | operationMap.put(StringUtils.lowerCase(operation.getName()), operation); 34 | }); 35 | } 36 | 37 | /** 38 | * Gets name. 39 | * 40 | * @return the name 41 | */ 42 | public String getName() { 43 | return name; 44 | } 45 | 46 | /** 47 | * converts operations to an GraphQLObjectType. 48 | * 49 | * @return the GraphQLObjectType 50 | */ 51 | public GraphQLObjectType asGraphQLObjectType() { 52 | return GraphQLObjectType.newObject().name(this.name).build(); 53 | } 54 | 55 | /** 56 | * converts operations to an ObjectTypeDefinition. 57 | * 58 | * @return the ObjectTypeDefinition 59 | */ 60 | public ObjectTypeDefinition asObjectTypeDefinition() { 61 | ObjectTypeDefinition objectTypeDefinition = createObjectTypeDefinition(); 62 | objectTypeDefinition.setName(name); 63 | return objectTypeDefinition; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/intuit/graphql/orchestrator/NestedPetsService.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import graphql.ExecutionInput; 5 | import graphql.GraphQLContext; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.concurrent.CompletableFuture; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Getter; 11 | import lombok.Setter; 12 | import org.assertj.core.util.Lists; 13 | 14 | public class NestedPetsService implements ServiceProvider { 15 | 16 | private List pets; 17 | 18 | NestedPetsService() { 19 | this.pets = createPets(); 20 | } 21 | 22 | private List createPets() { 23 | Pet p1 = new Pet("pet-1", "Charlie", 2, 200, Boolean.TRUE, "DOG"); 24 | Pet p2 = new Pet("pet-2", "Milo", 2, 20, Boolean.FALSE, "RABBIT"); 25 | Pet p3 = new Pet("pet-3", "Poppy", 5, 500, Boolean.TRUE, "CAT"); 26 | return Lists.list(p1, p2, p3); 27 | } 28 | 29 | @Override 30 | public String getNameSpace() { 31 | return "PETS"; 32 | } 33 | 34 | @Override 35 | public Map sdlFiles() { 36 | return TestHelper.getFileMapFromList("nested/books-pets-person/schema-pets.graphqls"); 37 | } 38 | 39 | @Override 40 | public CompletableFuture> query(ExecutionInput executionInput, 41 | GraphQLContext context) { 42 | return CompletableFuture.completedFuture( 43 | ImmutableMap.of("data", ImmutableMap.of("person", ImmutableMap.of("pets", this.pets)))); 44 | } 45 | 46 | @AllArgsConstructor 47 | @Getter 48 | @Setter 49 | private static class Pet { 50 | 51 | private String id; 52 | private String name; 53 | private Integer age; 54 | private Integer weight; 55 | private Boolean purebred; 56 | private String tag; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/resolverdirective/FieldNotFoundInParentExceptionSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective 2 | 3 | import graphql.ErrorType 4 | import spock.lang.Specification 5 | 6 | class FieldNotFoundInParentExceptionSpec extends Specification { 7 | 8 | static final String TEST_FIELD_NAME = "testFieldName" 9 | static final String TEST_PARENT_TYPE_NAME = "testParentTypeName" 10 | static final String TEST_SERVICE_NAMESPACE = "testServiceNamespace" 11 | 12 | private ResolverDirectiveDefinition mockResolverDirectiveDefinition 13 | 14 | def "can Create Field Not Found In Parent Exception"() { 15 | given: 16 | mockResolverDirectiveDefinition = Mock(ResolverDirectiveDefinition.class) 17 | 18 | FieldNotFoundInParentException.Builder builder = FieldNotFoundInParentException 19 | .builder() 20 | .serviceNameSpace(TEST_SERVICE_NAMESPACE) 21 | .fieldName(TEST_FIELD_NAME) 22 | .parentTypeName(TEST_PARENT_TYPE_NAME) 23 | .resolverDirectiveDefinition(mockResolverDirectiveDefinition) 24 | 25 | String expectedMessage = String.format('''Field not found in parent's resolved value. fieldName=%s, parentTypeName=%s, resolverDirectiveDefinition=%s, serviceNameSpace=%s''', 26 | TEST_FIELD_NAME, TEST_PARENT_TYPE_NAME, mockResolverDirectiveDefinition, TEST_SERVICE_NAMESPACE) 27 | 28 | when: 29 | FieldNotFoundInParentException fieldNotFoundInParentException = builder.build() 30 | 31 | then: 32 | fieldNotFoundInParentException != null 33 | fieldNotFoundInParentException.getMessage() == expectedMessage 34 | fieldNotFoundInParentException.getErrorType() == ErrorType.ExecutionAborted 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/integration/GraphQLOrchestratorSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.integration 2 | 3 | import graphql.ExecutionInput 4 | import graphql.GraphQLContext 5 | import graphql.language.Document 6 | import helpers.BaseIntegrationTestSpecification 7 | 8 | import java.util.function.BiConsumer 9 | 10 | import static graphql.language.AstPrinter.printAstCompact 11 | 12 | class GraphQLOrchestratorSpec extends BaseIntegrationTestSpecification { 13 | 14 | public static final BiConsumer USER_ASSERTS = { executionInput, _context -> 15 | String query = printAstCompact((Document) executionInput.getRoot()) 16 | if (query.contains("userFragment")) { 17 | //TODO: When RestExecutorBatchLoader supports Fragments 18 | assert executionInput.getQuery().contains("fragment", "userFragment", "on", "User") 19 | assert executionInput.getQuery().doesNotContain("bookFragment") 20 | assert executionInput.getQuery().doesNotContain("petFragment") 21 | } 22 | } 23 | 24 | public static final BiConsumer PET_ASSERTS = { executionInput, _context -> 25 | if (executionInput.getQuery().contains("petFragment")) { 26 | assert executionInput.getQuery().contains("fragment petFragment on Pet") 27 | assert executionInput.getQuery().doesNotContain("bookFragment") 28 | } 29 | } 30 | 31 | public static final BiConsumer BOOK_ASSERTS = { executionInput, _context -> 32 | if (executionInput.getQuery().contains("bookFragment")) { 33 | assert executionInput.getQuery().contains("fragment bookFragment on Book") 34 | assert executionInput.getQuery().doesNotContain("Pet") 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/utils/QueryDirectivesUtil.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils; 2 | 3 | import graphql.language.Argument; 4 | import graphql.language.BooleanValue; 5 | import graphql.language.Directive; 6 | import graphql.language.Field; 7 | import graphql.language.Value; 8 | import graphql.language.VariableReference; 9 | 10 | import java.util.Map; 11 | import java.util.Optional; 12 | 13 | public class QueryDirectivesUtil { 14 | 15 | public static boolean shouldIgnoreNode(Field node, Map queryVariables) { 16 | Optional optionalIncludesDir = node.getDirectives("include").stream().findFirst(); 17 | Optional optionalSkipDir = node.getDirectives("skip").stream().findFirst(); 18 | if(optionalIncludesDir.isPresent() || optionalSkipDir.isPresent()) { 19 | if(optionalIncludesDir.isPresent() && (!getIfValue(optionalIncludesDir.get(), queryVariables))) { 20 | return true; 21 | } 22 | return optionalSkipDir.isPresent() && (getIfValue(optionalSkipDir.get(), queryVariables)); 23 | } 24 | 25 | return false; 26 | } 27 | 28 | private static boolean getIfValue(Directive directive, Map queryVariables){ 29 | Argument ifArg = directive.getArgument("if"); 30 | Value ifValue = ifArg.getValue(); 31 | 32 | boolean defaultValue = directive.getName().equals("skip"); 33 | 34 | if(ifValue instanceof VariableReference) { 35 | String variableRefName = ((VariableReference) ifValue).getName(); 36 | return (boolean) queryVariables.getOrDefault(variableRefName, defaultValue); 37 | } else if(ifValue instanceof BooleanValue) { 38 | return ((BooleanValue) ifValue).isValue(); 39 | } 40 | return false; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/resolverdirective/FieldValueIsNullInParentExceptionSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective 2 | 3 | import graphql.ErrorType 4 | import spock.lang.Specification 5 | 6 | class FieldValueIsNullInParentExceptionSpec extends Specification { 7 | 8 | static final String TEST_SERVICE_NAMESPACE = "testServiceNamespace" 9 | static final String TEST_FIELD_NAME = "testFieldName" 10 | static final String TEST_PARENT_TYPE_NAME = "testParentTypeName" 11 | 12 | def "can Create Field Not Found In Parent Exception"() { 13 | given: 14 | def mockResolverDirectiveDefinition = Mock(ResolverDirectiveDefinition.class) 15 | 16 | def builder = FieldValueIsNullInParentException 17 | .builder() 18 | .serviceNameSpace(TEST_SERVICE_NAMESPACE) 19 | .fieldName(TEST_FIELD_NAME) 20 | .parentTypeName(TEST_PARENT_TYPE_NAME) 21 | .resolverDirectiveDefinition(mockResolverDirectiveDefinition) 22 | 23 | def expectedMessage = String.format( 24 | "Field value not found in parent's resolved value. " 25 | + " fieldName=%s, " 26 | + " parentTypeName=%s, " 27 | + " resolverDirectiveDefinition=%s," 28 | + " serviceNameSpace=%s", 29 | TEST_FIELD_NAME, TEST_PARENT_TYPE_NAME, mockResolverDirectiveDefinition, TEST_SERVICE_NAMESPACE) 30 | 31 | when: 32 | FieldValueIsNullInParentException fieldValueIsNullInParentException = builder.build() 33 | 34 | then: 35 | fieldValueIsNullInParentException != null 36 | fieldValueIsNullInParentException.getMessage() == expectedMessage 37 | fieldValueIsNullInParentException.getErrorType() == ErrorType.ExecutionAborted 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/datafetcher/ArgumentAppenderVisitor.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.datafetcher; 2 | 3 | import graphql.analysis.QueryVisitorFieldEnvironment; 4 | import graphql.analysis.QueryVisitorStub; 5 | import graphql.language.Argument; 6 | import graphql.language.Field; 7 | import graphql.schema.GraphQLFieldsContainer; 8 | import graphql.util.TraversalControl; 9 | import graphql.util.TreeTransformerUtil; 10 | import java.util.List; 11 | 12 | /** 13 | * This class is responsible for appending an argument list to a given field in a query. 14 | */ 15 | public class ArgumentAppenderVisitor extends QueryVisitorStub { 16 | 17 | private String expectedContainerTypeName; 18 | private String expectedFieldName; 19 | private List arguments; 20 | 21 | public ArgumentAppenderVisitor(final String expectedContainerTypeName, final String expectedFieldName, 22 | final List arguments) { 23 | this.expectedContainerTypeName = expectedContainerTypeName; 24 | this.expectedFieldName = expectedFieldName; 25 | this.arguments = arguments; 26 | } 27 | 28 | @Override 29 | public TraversalControl visitFieldWithControl(final QueryVisitorFieldEnvironment env) { 30 | if (!env.isTypeNameIntrospectionField() && isExpectedField(env.getFieldsContainer(), env.getField())) { 31 | Field newFieldWithArguments = env.getField().transform(builder -> builder.arguments(arguments)); 32 | 33 | TreeTransformerUtil.changeNode(env.getTraverserContext(), newFieldWithArguments); 34 | 35 | return TraversalControl.QUIT; 36 | } 37 | 38 | return TraversalControl.CONTINUE; 39 | } 40 | 41 | private boolean isExpectedField(GraphQLFieldsContainer containerType, Field field) { 42 | return this.expectedFieldName.equals(field.getName()) && expectedContainerTypeName 43 | .equals(containerType.getName()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/resolverdirective/ResolverArgumentNotAFieldOfParentExceptionSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective 2 | 3 | import spock.lang.Specification 4 | 5 | class ResolverArgumentNotAFieldOfParentExceptionSpec extends Specification { 6 | 7 | private String TEST_RESOLVER_ARG_VALUE = "testResolverArgValue" 8 | private String TEST_PARENT_TYPE_NAME = "testParentTypeName" 9 | private String TEST_REQUIRED_FIELD_NAME = "testReqdFieldName" 10 | private String TEST_SERVICE_NAME = "testServiceName" 11 | 12 | def "ResolverArgumentNotAFieldOfParentException with resolverArgValue and parentTypeName parameter forms a correct error message"() { 13 | given: 14 | String expectedMessage = "Resolver argument value testResolverArgValue should be a reference to a field in Parent Type testParentTypeName" 15 | ResolverArgumentNotAFieldOfParentException resolverArgumentNotAFieldOfParentException = 16 | new ResolverArgumentNotAFieldOfParentException(TEST_RESOLVER_ARG_VALUE, TEST_PARENT_TYPE_NAME) 17 | expect: 18 | resolverArgumentNotAFieldOfParentException.message == expectedMessage 19 | } 20 | 21 | def "ResolverArgumentNotAFieldOfParentException with reqdFieldName, serviceName, resolverArgValue and parentTypeName parameter forms a correct error message"() { 22 | given: 23 | String expectedMessage = "'testReqdFieldName' is not a field of parent type. serviceName=testServiceName, parentTypeName=testResolverArgValue, fieldName=testParentTypeName, " 24 | ResolverArgumentNotAFieldOfParentException resolverArgumentNotAFieldOfParentException = 25 | new ResolverArgumentNotAFieldOfParentException(TEST_REQUIRED_FIELD_NAME, TEST_SERVICE_NAME, TEST_RESOLVER_ARG_VALUE, TEST_PARENT_TYPE_NAME) 26 | expect: 27 | resolverArgumentNotAFieldOfParentException.message == expectedMessage 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/integration/SingleServiceSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.integration 2 | 3 | import com.intuit.graphql.orchestrator.GraphQLOrchestrator 4 | import com.intuit.graphql.orchestrator.PetsService 5 | import com.intuit.graphql.orchestrator.ServiceProvider 6 | import graphql.ExecutionInput 7 | import helpers.BaseIntegrationTestSpecification 8 | 9 | class SingleServiceSpec extends BaseIntegrationTestSpecification { 10 | 11 | private PetsService petsService = new PetsService(GraphQLOrchestratorSpec.PET_ASSERTS) 12 | 13 | def "test Multiple Top Level Field From Same Service"() { 14 | given: 15 | ServiceProvider[] services = [ petsService ] 16 | final GraphQLOrchestrator orchestrator = createGraphQLOrchestrator(services) 17 | 18 | // Test query using ExecutionInput 19 | ExecutionInput petsEI = ExecutionInput 20 | .newExecutionInput() 21 | .query(''' 22 | { 23 | pets { 24 | id name 25 | } 26 | pet(id : "pet-1") { 27 | id name 28 | } 29 | } 30 | ''') 31 | .build() 32 | 33 | when: 34 | Map executionResult = orchestrator.execute(petsEI).get() 35 | .toSpecification() 36 | 37 | then: 38 | noExceptionThrown() 39 | 40 | orchestrator.getSchema().isSupportingMutations() 41 | 42 | executionResult.get("errors") == null 43 | executionResult.get("data") != null 44 | 45 | Map dataValue = (Map) executionResult.get("data") 46 | dataValue.keySet().containsAll("pets", "pet") 47 | ((List>) dataValue.get("pets")).size() == 3 48 | ((Map) dataValue.get("pet")).size() == 2 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/beta_release.yml: -------------------------------------------------------------------------------- 1 | name: Beta Release to maven-central 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | releaseVersion: 6 | description: 'Beta Release version' 7 | required: true 8 | default: 'X.Y.Z-beta1' 9 | tag: 10 | description: 'Github Tag' 11 | required: true 12 | default: 'graphql-orchestrator-java-X.Y.Z-beta1' 13 | developmentVersion: 14 | description: 'Next Development version' 15 | required: true 16 | default: 'X.Y.Z-SNAPSHOT' 17 | 18 | 19 | jobs: 20 | publish: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - run: echo "Will release to central maven" 24 | 25 | - uses: actions/checkout@v2 26 | with: 27 | token: ${{ secrets.REPO_GIT_TOKEN }} 28 | 29 | - name: Set up Maven Central Repository 30 | uses: actions/setup-java@v1 31 | with: 32 | java-version: 11 33 | server-id: ossrh 34 | server-username: MAVEN_USERNAME 35 | server-password: MAVEN_PASSWORD 36 | gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} 37 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 38 | 39 | - name: Configure Git User 40 | run: | 41 | git config user.email "actions@github.com" 42 | git config user.name "GitHub Actions" 43 | # - name: Set projects Maven version to GitHub Action GUI set version 44 | # run: mvn versions:set "-DnewVersion=${{ github.event.inputs.releaseversion }}" 45 | 46 | - name: Publish package 47 | run: mvn --batch-mode -Dtag=${{ github.event.inputs.tag }} -DreleaseVersion=${{ github.event.inputs.releaseVersion }} -DdevelopmentVersion=${{ github.event.inputs.developmentVersion }} release:prepare release:perform -P release -DskipTests=true 48 | env: 49 | MAVEN_USERNAME: ${{ secrets.OSS_SONATYPE_USERNAME }} 50 | MAVEN_PASSWORD: ${{ secrets.OSS_SONATYPE_PASSWORD }} 51 | MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} 52 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/resolverdirective/ResolverArgumentDefinition.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective; 2 | 3 | import com.intuit.graphql.graphQL.NamedType; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | import java.util.function.Consumer; 8 | 9 | @Getter 10 | @ToString 11 | public class ResolverArgumentDefinition { 12 | 13 | private final String name; 14 | private final String value; 15 | 16 | @ToString.Exclude 17 | private final NamedType namedType; 18 | 19 | public ResolverArgumentDefinition(String name, String value) { 20 | this(name, value, null); 21 | } 22 | 23 | public ResolverArgumentDefinition(String name, String value, NamedType namedType) { 24 | this.name = name; 25 | this.value = value; 26 | this.namedType = namedType; 27 | } 28 | 29 | public ResolverArgumentDefinition transform(Consumer consumer) { 30 | ResolverArgumentDefinition.Builder builder = new ResolverArgumentDefinition.Builder(this); 31 | consumer.accept(builder); 32 | return builder.build(); 33 | } 34 | 35 | public static class Builder { 36 | 37 | private String name; 38 | private String value; 39 | private NamedType namedType; 40 | 41 | public Builder() { 42 | } 43 | 44 | public Builder(ResolverArgumentDefinition copy) { 45 | this.name = copy.getName(); 46 | this.value = copy.getValue(); 47 | } 48 | 49 | public ResolverArgumentDefinition.Builder name(String name) { 50 | this.name = name; 51 | return this; 52 | } 53 | 54 | public ResolverArgumentDefinition.Builder value(String value) { 55 | this.value = value; 56 | return this; 57 | } 58 | 59 | public ResolverArgumentDefinition.Builder namedType(NamedType namedType) { 60 | this.namedType = namedType; 61 | return this; 62 | } 63 | 64 | public ResolverArgumentDefinition build() { 65 | return new ResolverArgumentDefinition(name, value, namedType); 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/federation/metadata/KeyDirectiveMetadataSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.federation.metadata 2 | 3 | 4 | import com.intuit.graphql.graphQL.Directive 5 | import com.intuit.graphql.graphQL.DirectiveDefinition 6 | import com.intuit.graphql.graphQL.ValueWithVariable 7 | import graphql.language.Argument 8 | import spock.lang.Specification 9 | 10 | import static com.intuit.graphql.orchestrator.xtext.GraphQLFactoryDelegate.* 11 | 12 | class KeyDirectiveMetadataSpec extends Specification { 13 | def "KeyDirectiveMetadata from throws IllegalStateException for Directive with no arguments"() { 14 | given: 15 | Directive directive = generateKeyDirective("keyFieldName", false) 16 | when: 17 | KeyDirectiveMetadata.from(directive) 18 | then: 19 | thrown(IllegalStateException.class) 20 | } 21 | 22 | def "KeyDirectiveMetadata is returned by the from method for a Directive with arguments"() { 23 | given: 24 | Directive directive = generateKeyDirective("keyFieldName", true) 25 | KeyDirectiveMetadata keyDirectiveMetadata = KeyDirectiveMetadata.from(directive) 26 | 27 | expect: 28 | keyDirectiveMetadata != null 29 | 30 | } 31 | 32 | 33 | private Directive generateKeyDirective(String fieldSet, boolean addArgument) { 34 | ValueWithVariable fieldsInput = createValueWithVariable() 35 | fieldsInput.setStringValue(fieldSet) 36 | 37 | com.intuit.graphql.graphQL.Argument fieldsArgument = createArgument() 38 | fieldsArgument.setName("fields") 39 | fieldsArgument.setValueWithVariable(fieldsInput) 40 | 41 | DirectiveDefinition keyDefinition = createDirectiveDefinition() 42 | keyDefinition.setName("key") 43 | keyDefinition.setRepeatable(true) 44 | 45 | Directive keyDir = createDirective() 46 | keyDir.setDefinition(keyDefinition) 47 | if(addArgument) keyDir.getArguments().add(fieldsArgument) 48 | return keyDir 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/groovy/com/intuit/graphql/orchestrator/utils/GraphQLUtilSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.utils 2 | 3 | import graphql.language.ListType 4 | import graphql.language.NonNullType 5 | import graphql.language.Type 6 | import graphql.language.TypeName 7 | import graphql.schema.GraphQLList 8 | import graphql.schema.GraphQLNonNull 9 | import graphql.schema.GraphQLObjectType 10 | import spock.lang.Specification 11 | 12 | class GraphQLUtilSpec extends Specification { 13 | 14 | def "can Create Type From Graph QLObject Type"() { 15 | given: 16 | GraphQLObjectType graphQLObjectType = GraphQLObjectType.newObject().name("object").build() 17 | Type type = GraphQLUtil.createTypeBasedOnGraphQLType(graphQLObjectType) 18 | 19 | expect: 20 | type in TypeName 21 | ((TypeName) type).getName() == "object" 22 | } 23 | 24 | def "can Create Type From Non Null Type"() { 25 | given: 26 | GraphQLObjectType graphQLObjectType = GraphQLObjectType.newObject().name("object").build() 27 | GraphQLNonNull nonNull = GraphQLNonNull.nonNull(graphQLObjectType) 28 | 29 | Type type = GraphQLUtil.createTypeBasedOnGraphQLType(nonNull) 30 | Type nonNullType = ((NonNullType) type).getType() 31 | 32 | expect: 33 | type in NonNullType 34 | ((TypeName) nonNullType).getName() == "object" 35 | } 36 | 37 | def "can Create Type From List Type"() { 38 | given: 39 | GraphQLObjectType graphQLObjectType = GraphQLObjectType.newObject().name("object").build() 40 | GraphQLNonNull nonNull = GraphQLNonNull.nonNull(graphQLObjectType) 41 | GraphQLList graphQLList = GraphQLList.list(nonNull) 42 | 43 | Type type = GraphQLUtil.createTypeBasedOnGraphQLType(graphQLList) 44 | Type listType = ((ListType) type).getType() 45 | Type nonNullType = ((NonNullType) listType).getType() 46 | 47 | expect: 48 | type in ListType 49 | listType in NonNullType 50 | ((TypeName) nonNullType).getName() == "object" 51 | } 52 | } -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/datafetcher/FieldResolverDirectiveDataFetcher.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.datafetcher; 2 | 3 | import com.intuit.graphql.orchestrator.ServiceProvider.ServiceType; 4 | import com.intuit.graphql.orchestrator.batch.DataLoaderKeyUtil; 5 | import com.intuit.graphql.orchestrator.schema.transform.FieldResolverContext; 6 | import com.intuit.graphql.orchestrator.xtext.DataFetcherContext; 7 | import com.intuit.graphql.orchestrator.xtext.DataFetcherContext.DataFetcherType; 8 | import graphql.schema.DataFetchingEnvironment; 9 | import java.util.Objects; 10 | import lombok.Getter; 11 | 12 | @Getter 13 | public class FieldResolverDirectiveDataFetcher implements ServiceAwareDataFetcher { 14 | 15 | private final FieldResolverContext fieldResolverContext; 16 | private final String namespace; 17 | private final ServiceType serviceType; 18 | 19 | private FieldResolverDirectiveDataFetcher(FieldResolverContext fieldResolverContext, 20 | String namespace, ServiceType serviceType) { 21 | this.fieldResolverContext = fieldResolverContext; 22 | this.namespace = namespace; 23 | this.serviceType = serviceType; 24 | } 25 | 26 | public static FieldResolverDirectiveDataFetcher from(DataFetcherContext dataFetcherContext) { 27 | Objects.requireNonNull(dataFetcherContext, "DataFetcherContext is required"); 28 | Objects.requireNonNull(dataFetcherContext.getFieldResolverContext(), 29 | "FieldResolverContext is required"); 30 | return new FieldResolverDirectiveDataFetcher(dataFetcherContext.getFieldResolverContext(), 31 | dataFetcherContext.getNamespace(), dataFetcherContext.getServiceType()); 32 | } 33 | 34 | @Override 35 | public Object get(final DataFetchingEnvironment dataFetchingEnvironment) { 36 | return dataFetchingEnvironment 37 | .getDataLoader(DataLoaderKeyUtil.createDataLoaderKeyFrom(fieldResolverContext)) 38 | .load(dataFetchingEnvironment); 39 | } 40 | 41 | @Override 42 | public DataFetcherType getDataFetcherType() { 43 | return DataFetcherType.RESOLVER_ON_FIELD_DEFINITION; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/resolverdirective/ResolverArgumentTypeMismatch.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective; 2 | 3 | import com.intuit.graphql.orchestrator.xtext.FieldContext; 4 | 5 | /** 6 | * Exception will occur when requested argument type does not match with type in schema. 7 | * 8 | *
 9 |  *   {@code
10 |  *     field(arg: SomeInputType @resolver(field: "a.b.c"))
11 |  *
12 |  *     ...
13 |  *     type BType {
14 |  *       c: String # type in schema is String, but requested type is ObjectType!
15 |  *     }
16 |  *   }
17 |  * 
18 | */ 19 | public class ResolverArgumentTypeMismatch extends ResolverDirectiveException { 20 | 21 | private static final String MSG = "Resolver argument '%s' in '%s': Expected type '%s' to be '%s'."; 22 | 23 | private static final String MSG_WITH_PARENT_CONTEXT = "Resolver argument '%s' in '%s': Expected type '%s' in '%s' to be '%s'."; 24 | 25 | public ResolverArgumentTypeMismatch(final String argumentName, final FieldContext rootContext, 26 | final String inputTypeName, final String expectedTypeName) { 27 | super(String.format(MSG, argumentName, rootContext.toString(), inputTypeName, 28 | expectedTypeName)); 29 | } 30 | 31 | public ResolverArgumentTypeMismatch(final String argumentName, final FieldContext rootContext, 32 | final FieldContext parentContext, final String inputTypeName, final String expectedTypeName) { 33 | super(String 34 | .format(MSG_WITH_PARENT_CONTEXT, argumentName, rootContext.toString(), 35 | inputTypeName, parentContext.toString(), expectedTypeName)); 36 | } 37 | 38 | public static ResolverArgumentTypeMismatch create(final String argumentName, final FieldContext rootContext, 39 | final FieldContext parentContext, final String inputTypeName, final String expectedTypeName) { 40 | if (parentContext == null) { 41 | return new ResolverArgumentTypeMismatch(argumentName, rootContext, inputTypeName, expectedTypeName); 42 | } 43 | 44 | return new ResolverArgumentTypeMismatch(argumentName, rootContext, parentContext, inputTypeName, expectedTypeName); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/fieldresolver/ValueTemplate.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.fieldresolver; 2 | 3 | import com.intuit.graphql.orchestrator.schema.transform.FieldResolverContext; 4 | import org.apache.commons.collections4.CollectionUtils; 5 | import org.apache.commons.lang3.StringUtils; 6 | 7 | import java.util.Map; 8 | 9 | public class ValueTemplate { 10 | 11 | private FieldResolverContext fieldResolverContext; 12 | private String template; 13 | private boolean formatStringRef; 14 | 15 | public ValueTemplate(FieldResolverContext fieldResolverContext, String template) { 16 | this.fieldResolverContext = fieldResolverContext; 17 | this.template = template; 18 | this.formatStringRef = StringUtils.containsAny(template, "[", "{"); 19 | } 20 | public String compile(Map dataSource) { 21 | String resolverReferenceTemplate1 = "\"$%s\""; 22 | String resolverReferenceTemplate2 = "$%s"; 23 | 24 | //Create as new String to not interfere with resolver reference 25 | return CollectionUtils.isNotEmpty(fieldResolverContext.getRequiredFields()) ? fieldResolverContext.getRequiredFields().stream().reduce(template, (formattedTemplate, resolverRef) -> { 26 | String stringValue; 27 | Object resolverValue = dataSource.get(resolverRef); 28 | if(resolverValue == null) { 29 | stringValue = "null"; 30 | } else if(formatStringRef && resolverValue instanceof String) { 31 | stringValue = StringUtils.join("\"", resolverValue.toString(), "\""); 32 | } else { 33 | stringValue = resolverValue.toString(); 34 | } 35 | 36 | String resolverReference1 = String.format(resolverReferenceTemplate1, resolverRef); 37 | String resolverReference2 = String.format(resolverReferenceTemplate2, resolverRef); 38 | 39 | String replaceQuotedRef = StringUtils.replace(formattedTemplate, resolverReference1, stringValue); 40 | return StringUtils.replace(replaceQuotedRef, resolverReference2, stringValue); 41 | }) : template; 42 | } 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/graphql/orchestrator/resolverdirective/ResolverArgumentLeafTypeNotSame.java: -------------------------------------------------------------------------------- 1 | package com.intuit.graphql.orchestrator.resolverdirective; 2 | 3 | import com.intuit.graphql.orchestrator.xtext.FieldContext; 4 | 5 | /** 6 | * Exception will occur when either ScalarTypes are not referring to the same scalar type name. 7 | * 8 | *
 9 |  *  {@code
10 |  *    field(arg: Int @resolver(field: "a.b.c"))
11 |  *
12 |  *    ...type in schema
13 |  *
14 |  *    type BType {
15 |  *      c: String # type mismatch!
16 |  *    }
17 |  *
18 |  *  }
19 |  * 
20 | * For example, an exception should be thrown if the requested type is {@code Int} and the type in the schema is a 21 | * {@code String}. 22 | */ 23 | public class ResolverArgumentLeafTypeNotSame extends ResolverDirectiveException { 24 | 25 | private static final String MSG = "Resolver argument '%s' in '%s': Expected '%s' to be '%s'."; 26 | 27 | private static final String MSG_WITH_PARENT_CONTEXT = "Resolver argument '%s' in '%s': Expected '%s' in '%s' to be '%s'."; 28 | 29 | public ResolverArgumentLeafTypeNotSame(final String argumentName, FieldContext fieldContext, 30 | String foundScalarType, String expectedScalarType) { 31 | super(String.format(MSG, argumentName, fieldContext.toString(), foundScalarType, 32 | expectedScalarType)); 33 | } 34 | 35 | public ResolverArgumentLeafTypeNotSame(final String argumentName, FieldContext fieldContext, 36 | FieldContext inputTypeParentContext, String foundType, String expectedType) { 37 | super(String.format(MSG_WITH_PARENT_CONTEXT, argumentName, fieldContext.toString(), foundType, 38 | inputTypeParentContext.toString(), expectedType)); 39 | } 40 | 41 | public static ResolverArgumentLeafTypeNotSame create(final String argumentName, FieldContext fieldContext, 42 | FieldContext inputTypeParentContext, String foundType, String expectedType) { 43 | if (inputTypeParentContext == null) { 44 | return new ResolverArgumentLeafTypeNotSame(argumentName, fieldContext, foundType, expectedType); 45 | } 46 | 47 | return new ResolverArgumentLeafTypeNotSame(argumentName, fieldContext, inputTypeParentContext, foundType, 48 | expectedType); 49 | } 50 | } 51 | --------------------------------------------------------------------------------