graphql-engine/server/src-lib/Hasura/GraphQL/Parser/Variable.hs
Auke Booij 8ccf7724ce server: Metadata origin for definitions (type parameter version v2)
The code that builds the GraphQL schema, and `buildGQLContext` in particular, is partial: not every value of `(ServerConfigCtx, GraphQLQueryType, SourceCache, HashMap RemoteSchemaName (RemoteSchemaCtx, MetadataObject), ActionCache, AnnotatedCustomTypes)` results in a valid GraphQL schema. When it fails, we want to be able to return better error messages than we currently do.

The key thing that is missing is a way to trace back GraphQL type information to their origin from the Hasura metadata. Currently, we have a number of correctness checks of our GraphQL schema. But these correctness checks only have access to pure GraphQL type information, and hence can only report errors in terms of that. Possibly the worst is the "conflicting definitions" error, which, in practice, can only be debugged by Hasura engineers. This is terrible DX for customers.

This PR allows us to print better error messages, by adding a field to the `Definition` type that traces the GraphQL type to its origin in the metadata. So the idea is simple: just add `MetadataObjId`, or `Maybe` that, or some other sum type of that, to `Definition`.

However, we want to avoid having to import a `Hasura.RQL` module from `Hasura.GraphQL.Parser`. So we instead define this additional field of `Definition` through a new type parameter, which is threaded through in `Hasura.GraphQL.Parser`. We then define type synonyms in `Hasura.GraphQL.Schema.Parser` that fill in this type parameter, so that it is not visible for the majority of the codebase.

The idea of associating metadata information to `Definition`s really comes to fruition when combined with hasura/graphql-engine-mono#4517. Their combination would allow us to use the API of fatal errors (just like the current `MonadError QErr`) to report _inconsistencies_ in the metadata. Such inconsistencies are then _automatically_ ignored. So no ad-hoc decisions need to be made on how to cut out inconsistent metadata from the GraphQL schema. This will allow us to report much better errors, as well as improve the likelihood of a successful HGE startup.

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4770
Co-authored-by: Samir Talwar <47582+SamirTalwar@users.noreply.github.com>
GitOrigin-RevId: 728402b0cae83ae8e83463a826ceeb609001acae
2022-06-28 15:53:44 +00:00

114 lines
4.0 KiB
Haskell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

module Hasura.GraphQL.Parser.Variable
( InputValue (..),
Variable (..),
VariableInfo (..),
)
where
import Data.Aeson qualified as J
import Hasura.GraphQL.Parser.Names
import Hasura.Incremental (Cacheable)
import Hasura.Prelude
import Language.GraphQL.Draft.Syntax
( GType (..),
Name (..),
Value (..),
)
import Language.Haskell.TH.Lift qualified as TH
{- Note [Parsing variable values]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
GraphQL includes its own tiny language for input values, which is similar to
JSON but not quite the same---GraphQL input values can be enum values, and there
are restrictions on the names of input object keys. Despite these differences,
variables values are passed as JSON, so we actually need to be able to parse
values expressed in both languages.
Its tempting to contain this complexity by simply converting the JSON values to
GraphQL input values up front, and for booleans, numbers, arrays, and most
objects, this conversion is viable. But JSON strings pose a problem, since they
are used to represent both GraphQL strings and GraphQL enums. For example,
consider a query like this:
enum FooBar {
FOO
BAR
}
query some_query($a: String, $b: FooBar) {
...
}
We might receive an accompany variables payload like this:
{
"a": "FOO",
"b": "FOO"
}
To properly convert these JSON values to GraphQL, wed need to use the type
information to guide the parsing. Since $a has type String, its value should be
parsed as the GraphQL string "FOO", while $b has type FooBar, so its value
should be parsed as the GraphQL enum value FOO.
We could do this type-directed parsing, but there are some advantages to being
lazier. For one, we can use JSON values directly when used as a column value of
type json or jsonb, rather than converting them to GraphQL and back; which, in
turn, solves another problem with JSON objects: JSON object keys are arbitrary
strings, while GraphQL input object keys are GraphQL names, and therefore
restricted: not all JSON objects can be represented by a GraphQL input object.
Arguably such columns should really be represented as strings containing encoded
JSON, not GraphQL lists/objects, but the decision to treat them otherwise is
old, and it would be backwards-incompatible to change now. We can also avoid
needing to interpret the values of variables for types outside our control
(i.e. those from a remote schema), which can be useful in the case of custom
scalars or extensions of the GraphQL protocol.
So instead we use the InputValue type to represent that an input value might be
a GraphQL literal value or a JSON value from the variables payload. This means
each input parser constructor needs to be able to parse both GraphQL values and
JSON values, but fortunately, the duplication of logic is minimal. -}
-- | See Note [Parsing variable values].
data InputValue v
= GraphQLValue (Value v)
| JSONValue J.Value
deriving (Show, Eq, Functor, Generic, Ord, TH.Lift)
instance (Hashable v) => Hashable (InputValue v)
instance (Cacheable v) => Cacheable (InputValue v)
data Variable = Variable
{ vInfo :: VariableInfo,
vType :: GType,
-- | Note: if the variable was null or was not provided and the field has a
-- non-null default value, this field contains the default value, not 'VNull'.
vValue :: InputValue Void
}
deriving (Show, Eq, Generic, Ord, TH.Lift)
instance Hashable Variable
instance Cacheable Variable
instance HasName Variable where
getName = getName . vInfo
data VariableInfo
= VIRequired Name
| -- | Unlike fields (see 'InputFieldInfo'), nullable variables with no
-- default value are indistinguishable from variables with a default value
-- of null, so we dont distinguish those cases here.
VIOptional Name (Value Void)
deriving (Show, Eq, Generic, Ord, TH.Lift)
instance Hashable VariableInfo
instance Cacheable VariableInfo
instance HasName VariableInfo where
getName (VIRequired name) = name
getName (VIOptional name _) = name