mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-18 04:51:35 +03:00
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
114 lines
4.0 KiB
Haskell
114 lines
4.0 KiB
Haskell
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.
|
||
|
||
It’s 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, we’d 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 don’t 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
|