haskell-graphql / graphql-api

Write type-safe GraphQL services in Haskell
BSD 3-Clause "New" or "Revised" License
406 stars 35 forks source link

ValidationError VariableTypeNotFound despite presence in the schema #204

Open EdmundsEcho opened 6 years ago

EdmundsEcho commented 6 years ago

Might anyone know what might cause the VariableTypeNotFound error from compileQuery being generated despite, what seems like a valid schema that includes the type requested type definition?

Here is what I'm seeing: The call

  let query =
        compileQuery
        schema 
        [r|query($vi: RequestInput!) {     

             wrap {       # Object :: Commands.Validate 
                          # (wraps a Field "validate" with Argument "request" :: RequestInput)

              validate(request: $vi)  {
                      subReq  { subjectType }
                      meaReqs { measurementType }
               }
             }
           }
           |]

... generates a Left error implying a missing type definition

Left (ValidationError (VariableTypeNotFound (Variable (Name {unName = "vi"}))
(Name {unName = "RequestInput"}) :| []))

... despite the definition being present in the successful makeSchema call

-- snippet of the compiled schema

... (Name {unName = "Validate"},TypeDefinitionObject (ObjectTypeDefinition
(Name {unName = "Validate"}) [] (FieldDefinition (Name {unName = "validate"})
[ArgumentDefinition (Name {unName = "request"}) 
(TypeNonNull (NonNullTypeNamed 
(DefinedInputType 
(InputTypeDefinitionObject 
(InputObjectTypeDefinition 
(Name {unName = "RequestInput"})       <<< definition of RequestInput

(InputObjectFieldDefinition (Name {unName ="subReq"}) (TypeNonNull (NonNullTypeNamed (DefinedInputType (InputTypeDefinitionObject (InputObjectTypeDefinition (Name {unName =
"QualityMixInput"}) (InputObjectFieldDefinition (Name {unName = "subjectType"})
(TypeNamed (BuiltinInputType GString)) Nothing :| [InputObjectFieldDefinition
(Name {unName = "qualityMix"}) ... etc.

Thank you to anyone that might have some ideas.

- E

theobat commented 6 years ago

hey @EdmundsEcho can you try to execute: lookupType yourSchema "RequestInput" And see what it returns ?

EdmundsEcho commented 6 years ago

Great question. It returns Nothing...

compileQuery uses lookupType to validate the 'TypeCondition'.

-- | Validate a type condition that appears in a query.
validateTypeCondition :: Schema -> AST.TypeCondition -> Validation TypeDefinition
validateTypeCondition schema (NamedType typeCond) =
  case lookupType schema typeCond of                  <<< here
    Nothing -> throwE (TypeConditionNotFound typeCond)
    Just typeDefn -> pure typeDefn

My schema describes the DefinedInputType among other Input related objects. However, the schema does not record a inferred TypeDefinitionObject that lookupType might find.

makeSchema calls the type class DefinesTypes function getDefinedTypes :: t -> Map Name TypeDefinition.

The InputType instance of DefinesTypes points to

DefinedInputType typeDefinition -> getDefinedTypes typeDefinition

In my case the DefinedInputType typeDefinition pattern matches to:

(DefinedInputType
(InputTypeDefinitionObject 
(InputObjectTypeDefinition (Name {unName = "RequestInput"}) 
                           (InputObjectFieldDefinition (Name {unName = etc... :| [])..

The getDefinedTypes function cascade points to the following

Value type constructor :: Type
-----------------------------------------------
DefinedInputType InputTypeDefinition :: InputType ->
InputTypeDefinitionObject InputObjectTypeDefinition :: InputTypeDefinition ->
InputObjectTypeDefinition Name (NonEmpty InputObjectFieldDefinition) :: InputObjectTypeDefinition ->   <<< here
InputObjectFieldDefinition Name (AnnotatedType InputType) (Maybe DefaultValue) :: InputObjectFieldDefinition -> 
TypeNonNull :: AnnotatedType etc...

If the InputObjectTypeDefinition is used to instantiate a TypeDefinition during the compileQuery operation, the getDefinedTypes instance evaluates to mempty

TypeDefinitionInputObject InputObjectTypeDefinition ->
TypeDefinitionInputObject _ -> mempty

If only to promote some ideas, if this is what is going on, then perhaps there are two things to consider:

  1. Should the library ever combine type definitions for InputObjectTypes with the regular ObjectTypes (e.g., combined in the same Map collection)?
  2. Should the compileQuery look for valid types in two separate collections of types, one for InputObjectTypes and the other for the regular ObjectTypes?
  3. Other explanation and thus consideration?

- E

EdmundsEcho commented 6 years ago

One last observation for this evening. In the GraphQL.Internal.Validation module the getInputTypeDefinition relies on lookupType.

-- | Ensure that a variable has a correct type declaration given a schema.
validateTypeAssertion :: Schema -> Variable -> AST.GType -> Validation (AnnotatedType InputType)
validateTypeAssertion schema var varTypeAST =
  astAnnotationToSchemaAnnotation varTypeAST <$>
  case lookupType schema varTypeNameAST of
    Nothing -> validateVariableTypeBuiltin var varTypeNameAST
    Just cleanTypeDef -> validateVariableTypeDefinition var cleanTypeDef
  where 
    varTypeNameAST = getName varTypeAST

-- | Validate a variable type which has a type definition in the schema.
validateVariableTypeDefinition :: Variable -> TypeDefinition -> Validation InputType
validateVariableTypeDefinition var typeDef = 
  case getInputTypeDefinition typeDef of 
    Nothing -> throwE (VariableTypeIsNotInputType var $ getName typeDef)
    Just value -> pure (DefinedInputType value)

Conclusion: lookupType is being relied on to retrieve both ObjectTypes and ObjectInputTypes. Based on what I outlined in the previous post, the getDefinedTypes used to populate the collection searched by lookupType does not seem to record ObjectInputTypes defined in the Schema.

EdmundsEcho commented 5 years ago

Does the library support using variables :: InputObject, instances of hasAnnotatedInputType, in a query that gets compiled at runtime? Has anyone had success doing so? Thank you in advance. - E