mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-24 07:52:14 +03:00
8ccf7724ce
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
934 lines
38 KiB
Haskell
934 lines
38 KiB
Haskell
{-# LANGUAGE PatternSynonyms #-}
|
||
|
||
-- | Types for representing a GraphQL schema.
|
||
module Hasura.GraphQL.Parser.Schema
|
||
( -- * Kinds
|
||
Kind (..),
|
||
(:<:) (..),
|
||
type (<:) (..),
|
||
|
||
-- * Types
|
||
Nullability (..),
|
||
Type (..),
|
||
onTypeDef,
|
||
TypeInfo (TIScalar, TIEnum, TIInputObject, TIObject, TIInterface, TIUnion),
|
||
getTypeInfo,
|
||
SomeDefinitionTypeInfo (..),
|
||
eqType,
|
||
eqTypeInfo,
|
||
typeNullability,
|
||
nullableType,
|
||
nonNullableType,
|
||
toGraphQLType,
|
||
getObjectInfo,
|
||
getInterfaceInfo,
|
||
EnumValueInfo (..),
|
||
InputFieldInfo (..),
|
||
FieldInfo (..),
|
||
InputObjectInfo (InputObjectInfo),
|
||
ObjectInfo (ObjectInfo, oiFields, oiImplements),
|
||
InterfaceInfo (InterfaceInfo, iiFields, iiPossibleTypes),
|
||
UnionInfo (UnionInfo, uiPossibleTypes),
|
||
|
||
-- * Definitions
|
||
Definition (..),
|
||
|
||
-- * Schemas
|
||
Schema (..),
|
||
ConflictingDefinitions (..),
|
||
HasTypeDefinitions (..),
|
||
TypeDefinitionsWrapper (..),
|
||
collectTypeDefinitions,
|
||
|
||
-- * Miscellany
|
||
DirectiveInfo (..),
|
||
)
|
||
where
|
||
|
||
import Control.Lens
|
||
import Data.Functor.Classes
|
||
import Data.HashMap.Strict.Extended qualified as Map
|
||
import Data.Hashable (Hashable (..))
|
||
import Data.List qualified as List
|
||
import Data.List.NonEmpty qualified as NE
|
||
import Data.Text qualified as T
|
||
import Data.Text.Extended
|
||
import Hasura.GraphQL.Parser.Names
|
||
import Hasura.Prelude
|
||
import Language.GraphQL.Draft.Syntax
|
||
( Description (..),
|
||
DirectiveLocation (..),
|
||
GType (..),
|
||
Name (..),
|
||
Value (..),
|
||
)
|
||
import Language.GraphQL.Draft.Syntax qualified as G
|
||
|
||
-- | GraphQL types are divided into two classes: input types and output types.
|
||
-- The GraphQL spec does not use the word “kind” to describe these classes, but
|
||
-- it’s an apt term.
|
||
--
|
||
-- Some GraphQL types can be used at either kind, so we also include the 'Both'
|
||
-- kind, the superkind of both 'Input' and 'Output'. The '<:' class provides
|
||
-- kind subsumption constraints.
|
||
--
|
||
-- For more details, see <http://spec.graphql.org/June2018/#sec-Input-and-Output-Types>.
|
||
data Kind
|
||
= -- | see Note [The 'Both kind]
|
||
Both
|
||
| Input
|
||
| Output
|
||
|
||
{- Note [The 'Both kind]
|
||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||
As described in the Haddock comments for Kind and <:, we use Kind to index
|
||
various types, such as Type and Parser. We use this to enforce various
|
||
correctness constraints mandated by the GraphQL spec; for example, we don’t
|
||
allow input object fields to have output types and we don’t allow output object
|
||
fields to have input types.
|
||
|
||
But scalars and enums can be used as input types *or* output types. A natural
|
||
encoding of that in Haskell would be to make constructors for those types
|
||
polymorphic, like this:
|
||
|
||
data Kind = Input | Output
|
||
|
||
data TypeInfo k where
|
||
TIScalar :: TypeInfo k -- \ Polymorphic!
|
||
TIEnum :: ... -> TypeInfo k -- /
|
||
TIInputObject :: ... -> TypeInfo origin 'Input
|
||
TIObject :: ... -> TypeInfo origin 'Output
|
||
|
||
Naturally, this would give the `scalar` parser constructor a similarly
|
||
polymorphic type:
|
||
|
||
scalar
|
||
:: MonadParse m
|
||
=> Name
|
||
-> Maybe Description
|
||
-> ScalarRepresentation a
|
||
-> Parser k m a -- Polymorphic!
|
||
|
||
But if we actually try that, we run into problems. The trouble is that we want
|
||
to use the Kind to influence several different things:
|
||
|
||
* As mentioned above, we use it to ensure that the types we generate are
|
||
well-kinded according to the GraphQL spec rules.
|
||
|
||
* We use it to determine what a Parser consumes as input. Parsers for input
|
||
types parse GraphQL input values, but Parsers for output types parse
|
||
selection sets. (See Note [The meaning of Parser 'Output] in
|
||
Hasura.GraphQL.Parser.Internal.Parser for an explanation of why.)
|
||
|
||
* We use it to know when to expect a sub-selection set for a field of an
|
||
output object (see Note [The delicate balance of GraphQL kinds]).
|
||
|
||
These many uses of Kind cause some trouble for a polymorphic representation. For
|
||
example, consider our `scalar` parser constructor above---if we were to
|
||
instantiate it at kind 'Output, we’d receive a `Parser 'Output`, which we would
|
||
then expect to be able to apply to a selection set. But that doesn’t make any
|
||
sense, since scalar fields don’t have selection sets!
|
||
|
||
Another issue with this representation has to do with effectful parser
|
||
constructors (such as constructors that can throw errors). These have types like
|
||
|
||
mkFooParser :: MonadSchema n m => Blah -> m (Parser k n Foo)
|
||
|
||
where the parser construction is itself monadic. This causes some annoyance,
|
||
since even if mkFooParser returns a Parser of a polymorphic kind, code like this
|
||
will not typecheck:
|
||
|
||
(fooParser :: forall k. Parser k n Foo) <- mkFooParser blah
|
||
|
||
The issue is that we have to instantiate k to a particular type to be able to
|
||
call mkFooParser. If we want to use the result at both kinds, we’d have to call
|
||
mkFooParser twice:
|
||
|
||
(fooInputParser :: Parser 'Input n Foo) <- mkFooParser blah
|
||
(fooOutputParser :: Parser 'Output n Foo) <- mkFooParser blah
|
||
|
||
Other situations encounter similar difficulties, and they are not easy to
|
||
resolve without impredicative polymorphism (which GHC does not support).
|
||
|
||
To avoid this problem, we don’t use polymorphic kinds, but instead introduce a
|
||
form of kind subsumption. Types that can be used as both input and output types
|
||
are explicitly given the kind 'Both. This allows us to get the best of both
|
||
worlds:
|
||
|
||
* We use the <: typeclass to accept 'Both in most places where we expect
|
||
either input or output types.
|
||
|
||
* We can treat 'Both specially to avoid requiring `scalar` to supply a
|
||
selection set parser (see Note [The delicate balance of GraphQL kinds] for
|
||
further explanation).
|
||
|
||
* Because we avoid the polymorphism, we don’t run into the aforementioned
|
||
issue with monadic parser constructors.
|
||
|
||
All of this is subtle and somewhat complicated, but unfortunately there isn’t
|
||
much of a way around that: GraphQL is subtle and complicated. Our use of an
|
||
explicit 'Both kind isn’t the only way to encode these things, but it’s the
|
||
particular set of compromises we’ve chosen to accept.
|
||
|
||
Note [The delicate balance of GraphQL kinds]
|
||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
As discussed in Note [The 'Both kind], we use GraphQL kinds to distinguish
|
||
several different things. One of them is which output types take sub-selection
|
||
sets. For example, scalars don’t accept sub-selection sets, so if we have a
|
||
schema like
|
||
|
||
type Query {
|
||
users: [User!]!
|
||
}
|
||
|
||
type User {
|
||
id: Int!
|
||
}
|
||
|
||
then the following query is illegal:
|
||
|
||
query {
|
||
users {
|
||
id {
|
||
blah
|
||
}
|
||
}
|
||
}
|
||
|
||
The id field has a scalar type, so it should not take a sub-selection set. This
|
||
is actually something we care about distinguishing at the type level, because it
|
||
affects the type of the `selection` parser combinator. Suppose we have a
|
||
`Parser 'Output m UserQuery` for the User type. When we parse a field with that
|
||
type, we expect to receive a UserQuery as a result, unsurprisingly. But what if
|
||
we parse an output field using the `int` parser, which has this type:
|
||
|
||
int :: MonadParse m => Parser 'Both m Int32
|
||
|
||
If we follow the same logic as for the User parser above, we’d expect to receive
|
||
an Int32 as a result... but that doesn’t make any sense, since the Int32
|
||
corresponds to the result *we* are suppose to produce as a result of executing
|
||
the query, not something user-specified.
|
||
|
||
One way to solve this would be to associate every Parser with two result types:
|
||
one when given an input object, and one when given a selection set. Then our
|
||
parsers could be given these types, instead:
|
||
|
||
user :: MonadParse m => Parser 'Output m Void UserQuery
|
||
int :: MonadParse m => Parser 'Both m Int32 ()
|
||
|
||
But if you work through this, you’ll find that *all* parsers will either have
|
||
Void or () for at least one of their input result types or their output result
|
||
types, depending on their kind:
|
||
|
||
* All 'Input parsers must have Void for their output result type, since they
|
||
aren’t allowed to be used in output contexts at all.
|
||
|
||
* All 'Output parsers must have Void for their input result type, since they
|
||
aren’t allowed to be used in input contexts at all.
|
||
|
||
* That just leaves 'Both. The only types of kind 'Both are scalars and enums,
|
||
neither of which accept a sub-selection set. Their output result type would
|
||
therefore be (), since they are allowed to appear in output contexts, but
|
||
they don’t return any results.
|
||
|
||
The end result of this is that we clutter all our types with Voids and ()s, with
|
||
little actual benefit.
|
||
|
||
If you really think about it, the fact that the no types of kind 'Both accept a
|
||
sub-selection set is really something of a coincidence. In theory, one could
|
||
imagine a future version of the GraphQL spec adding a type that can be used as
|
||
both an input type or an output type, but accepts a sub-selection set. If that
|
||
ever happens, we’ll have to tweak our encoding, but for now, we can take
|
||
advantage of this happy coincidence and make the kinds serve double duty:
|
||
|
||
* We can make `ParserInput 'Both` identical to `ParserInput 'Input`, since
|
||
all parsers of kind 'Both only parse input values.
|
||
|
||
* We can require types of kind 'Both in `selection`, which does not expect a
|
||
sub-selection set, and types of kind 'Output in `subselection`, which does.
|
||
|
||
Relying on this coincidence might seem a little gross, and perhaps it is
|
||
somewhat. But it’s enormously convenient: not doing this would make some types
|
||
significantly more complicated, since we would have to thread around more
|
||
information at the type level and we couldn’t make as many simplifying
|
||
assumptions. So until GraphQL adds a type that violates these assumptions, we
|
||
are happy to take advantage of this coincidence. -}
|
||
|
||
-- | Evidence for '<:'.
|
||
data k1 :<: k2 where
|
||
KRefl :: k :<: k
|
||
KBoth :: k :<: 'Both
|
||
|
||
-- | 'Kind' subsumption. The GraphQL kind hierarchy is extremely simple:
|
||
--
|
||
-- > Both
|
||
-- > / \
|
||
-- > Input Output
|
||
--
|
||
-- Various functions in this module use '<:' to allow 'Both' to be used in
|
||
-- places where 'Input' or 'Output' would otherwise be expected.
|
||
class k1 <: k2 where
|
||
subKind :: k1 :<: k2
|
||
|
||
instance k1 ~ k2 => k1 <: k2 where
|
||
subKind = KRefl
|
||
|
||
instance {-# OVERLAPPING #-} k <: 'Both where
|
||
subKind = KBoth
|
||
|
||
data Nullability = Nullable | NonNullable
|
||
deriving (Eq)
|
||
|
||
isNullable :: Nullability -> Bool
|
||
isNullable Nullable = True
|
||
isNullable NonNullable = False
|
||
|
||
data Type origin k
|
||
= TNamed Nullability (Definition origin (TypeInfo origin k))
|
||
| TList Nullability (Type origin k)
|
||
|
||
instance Eq (Type origin k) where
|
||
(==) = eqType
|
||
|
||
-- | Adjust the 'Definition' underlying a 'Type'
|
||
onTypeDef :: (forall a. Definition origin a -> Definition origin a) -> Type origin k -> Type origin k
|
||
onTypeDef f (TNamed nul def) = TNamed nul (f def)
|
||
onTypeDef f (TList nul typ) = TList nul (onTypeDef f typ)
|
||
|
||
-- | Like '==', but can compare 'Type's of different kinds.
|
||
eqType :: Type origin k1 -> Type origin k2 -> Bool
|
||
eqType (TNamed n a) (TNamed n' b) = n == n' && liftEq eqTypeInfo a b
|
||
eqType (TList n a) (TList n' b) = n == n' && eqType a b
|
||
eqType _ _ = False
|
||
|
||
instance HasName (Type origin k) where
|
||
getName (TNamed _ def) = getName def
|
||
getName (TList _ t) = getName t
|
||
|
||
typeNullability :: Type origin k -> Nullability
|
||
typeNullability (TNamed n _) = n
|
||
typeNullability (TList n _) = n
|
||
|
||
nullableType :: Type origin k -> Type origin k
|
||
nullableType (TNamed _ def) = TNamed Nullable def
|
||
nullableType (TList _ t) = TList Nullable t
|
||
|
||
nonNullableType :: Type origin k -> Type origin k
|
||
nonNullableType (TNamed _ def) = TNamed NonNullable def
|
||
nonNullableType (TList _ t) = TList NonNullable t
|
||
|
||
toGraphQLType :: Type origin k -> GType
|
||
toGraphQLType (TNamed n typeInfo) = TypeNamed (G.Nullability (isNullable n)) $ getName typeInfo
|
||
toGraphQLType (TList n typeInfo) = TypeList (G.Nullability (isNullable n)) $ toGraphQLType typeInfo
|
||
|
||
{- Note [The interfaces story]
|
||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
GraphQL interfaces are not conceptually complicated, but they pose some
|
||
non-obvious challenges for our implementation. First, familiarize yourself with
|
||
GraphQL interfaces themselves:
|
||
|
||
* https://graphql.org/learn/schema/#interfaces
|
||
* http://spec.graphql.org/June2018/#sec-Interfaces
|
||
* http://spec.graphql.org/June2018/#sec-Objects
|
||
|
||
The most logical repesentation of object and interface types is to have objects
|
||
reference the interfaces they implement, but not the other way around. After
|
||
all, that’s how it works in the GraphQL language: when you declare an interface,
|
||
you just specify its fields, and you specify which interfaces each object type
|
||
implements as part of their declarations.
|
||
|
||
However, this representation is actually not very useful for us. We /also/ need
|
||
the interfaces to reference the objects that implement them---forming a circular
|
||
structure---for two reasons:
|
||
|
||
1. Most directly, we need this information for introspection queries.
|
||
Introspection queries for object types return the set of interfaces they
|
||
implement <http://spec.graphql.org/June2018/#sec-Object>, and introspection
|
||
queries for interfaces return the set of object types that implement them
|
||
<http://spec.graphql.org/June2018/#sec-Interface>.
|
||
|
||
2. Less obviously, it’s more natural to specify the relationships “backwards”
|
||
like this when building the schema using the parser combinator language.
|
||
|
||
From the parser’s point of view, each implementation of an interface
|
||
corresponds to a distinct parsing possibility. For example, when we
|
||
generate a Relay schema, the type of the `node` root field is an interface,
|
||
and each table is a type that implements it:
|
||
|
||
type query_root {
|
||
node(id: ID!): Node
|
||
...
|
||
}
|
||
|
||
interface Node {
|
||
id: ID!
|
||
}
|
||
|
||
type author implements Node {
|
||
id: ID!
|
||
name: String!
|
||
...
|
||
}
|
||
|
||
type article implements Node {
|
||
id: ID!
|
||
title: String!
|
||
body: String!
|
||
...
|
||
}
|
||
|
||
A query will use fragments on the Node type to access table-specific fields:
|
||
|
||
query get_article_info($article_id: ID!) {
|
||
node(id: $article_id) {
|
||
... on article {
|
||
title
|
||
body
|
||
}
|
||
}
|
||
}
|
||
|
||
The query parser needs to know which types implement the interface (and
|
||
how to parse their selection sets) so that it can parse the fragments.
|
||
|
||
This presents some complications, since we need to build this information in a
|
||
circular fashion. Currently, we do this in a very naïve way:
|
||
|
||
* We require selectionSetObject to specify the interfaces it implements /and/
|
||
require selectionSetInterface to specify the objects that implement it.
|
||
|
||
* We take advantage of our existing memoization mechanism to do the knot-tying
|
||
for us (see Note [Tying the knot] in Hasura.GraphQL.Parser.Class).
|
||
|
||
You may notice that this makes it possible for the definitions to be
|
||
inconsistent: we could construct an interface parser that parses some object
|
||
type, but forget to specify that the object type implements the interface. This
|
||
inconsistency is currently completely unchecked, which is quite unfortunate. It
|
||
also means we don’t support remote schema-defined object types that implement
|
||
interfaces we generate, since we don’t know anything about those types when we
|
||
construct the interface.
|
||
|
||
Since we don’t make very much use of interface types at the time of this
|
||
writing, this isn’t much of a problem in practice. But if that changes, it would
|
||
be worth implementing a more sophisticated solution that can gather up all the
|
||
different sources of information and make sure they’re consistent. -}
|
||
|
||
-- | Invariant: the list is sorted by 'dName'
|
||
data InputObjectInfo origin = InputObjectInfo__ ~[Definition origin (InputFieldInfo origin)]
|
||
|
||
-- Public interface enforcing invariants
|
||
pattern InputObjectInfo :: [Definition origin (InputFieldInfo origin)] -> InputObjectInfo origin
|
||
pattern InputObjectInfo xs <-
|
||
InputObjectInfo__ xs
|
||
where
|
||
InputObjectInfo xs = InputObjectInfo__ (List.sortOn dName xs)
|
||
|
||
{-# COMPLETE InputObjectInfo #-}
|
||
|
||
-- Note that we can't check for equality of the fields since there may be
|
||
-- circularity. So we rather check for equality of names.
|
||
instance Eq (InputObjectInfo origin) where
|
||
InputObjectInfo fields1 == InputObjectInfo fields2 =
|
||
eqByName fields1 fields2
|
||
|
||
-- | Invariant: the lists are sorted by 'dName', maintained via pattern synonyms
|
||
data ObjectInfo origin = ObjectInfo__
|
||
{ -- | The fields that this object has. This consists of the fields of the
|
||
-- interfaces that it implements, as well as any additional fields.
|
||
_oiFields :: ~[Definition origin (FieldInfo origin)],
|
||
-- | The interfaces that this object implements (inheriting all their
|
||
-- fields). See Note [The interfaces story] for more details.
|
||
_oiImplements :: ~[Definition origin (InterfaceInfo origin)]
|
||
}
|
||
|
||
-- Public interface enforcing invariants
|
||
pattern ObjectInfo :: [Definition origin (FieldInfo origin)] -> [Definition origin (InterfaceInfo origin)] -> ObjectInfo origin
|
||
pattern ObjectInfo {oiFields, oiImplements} <-
|
||
ObjectInfo__ oiFields oiImplements
|
||
where
|
||
ObjectInfo xs ys = ObjectInfo__ (List.sortOn dName xs) (List.sortOn dName ys)
|
||
|
||
{-# COMPLETE ObjectInfo #-}
|
||
|
||
-- | Note that we can't check for equality of the fields and the interfaces since
|
||
-- there may be circularity. So we rather check for equality of names.
|
||
--
|
||
-- This is dodgy... the equality logic here should I think correspond to the
|
||
-- logic in @typeField@ and its neighbors in "Hasura.GraphQL.Schema.Introspect",
|
||
-- in terms of how much we recurse.
|
||
instance Eq (ObjectInfo origin) where
|
||
ObjectInfo fields1 interfaces1 == ObjectInfo fields2 interfaces2 =
|
||
eqByName fields1 fields2 && eqByName interfaces1 interfaces2
|
||
|
||
-- | Type information for a GraphQL interface; see Note [The interfaces story]
|
||
-- for more details.
|
||
--
|
||
-- Note: in the current working draft of the GraphQL specification (> June
|
||
-- 2018), interfaces may implement other interfaces, but we currently don't
|
||
-- support this.
|
||
--
|
||
-- Invariant: the lists are sorted by 'dName', maintained via pattern synonyms
|
||
data InterfaceInfo origin = InterfaceInfo__
|
||
{ -- | Fields declared by this interface. Every object implementing this
|
||
-- interface must include those fields.
|
||
_iiFields :: ~[Definition origin (FieldInfo origin)],
|
||
-- | Objects that implement this interface. See Note [The interfaces story]
|
||
-- for why we include that information here.
|
||
_iiPossibleTypes :: ~[Definition origin (ObjectInfo origin)]
|
||
}
|
||
|
||
-- Public interface enforcing invariants
|
||
pattern InterfaceInfo :: [Definition origin (FieldInfo origin)] -> [Definition origin (ObjectInfo origin)] -> InterfaceInfo origin
|
||
pattern InterfaceInfo {iiFields, iiPossibleTypes} <-
|
||
InterfaceInfo__ iiFields iiPossibleTypes
|
||
where
|
||
InterfaceInfo xs ys = InterfaceInfo__ (List.sortOn dName xs) (List.sortOn dName ys)
|
||
|
||
{-# COMPLETE InterfaceInfo #-}
|
||
|
||
-- Note that we can't check for equality of the fields and the interfaces since
|
||
-- there may be circularity. So we rather check for equality of names.
|
||
instance Eq (InterfaceInfo origin) where
|
||
InterfaceInfo fields1 objects1 == InterfaceInfo fields2 objects2 =
|
||
eqByName fields1 fields2 && eqByName objects1 objects2
|
||
|
||
-- | Invariant: the list is sorted by 'dName'
|
||
data UnionInfo origin = UnionInfo__
|
||
{ -- | The member object types of this union.
|
||
_uiPossibleTypes :: ~[Definition origin (ObjectInfo origin)]
|
||
}
|
||
|
||
-- Public interface enforcing invariants
|
||
pattern UnionInfo :: [Definition origin (ObjectInfo origin)] -> UnionInfo origin
|
||
pattern UnionInfo {uiPossibleTypes} <-
|
||
UnionInfo__ uiPossibleTypes
|
||
where
|
||
UnionInfo xs = UnionInfo__ (List.sortOn dName xs)
|
||
|
||
{-# COMPLETE UnionInfo #-}
|
||
|
||
data TypeInfo origin k where
|
||
TIScalar :: TypeInfo origin 'Both
|
||
-- | Invariant: the NonEmpty is sorted by 'dName'
|
||
TIEnum__ :: NonEmpty (Definition origin EnumValueInfo) -> TypeInfo origin 'Both
|
||
TIInputObject :: InputObjectInfo origin -> TypeInfo origin 'Input
|
||
TIObject :: ObjectInfo origin -> TypeInfo origin 'Output
|
||
TIInterface :: InterfaceInfo origin -> TypeInfo origin 'Output
|
||
TIUnion :: UnionInfo origin -> TypeInfo origin 'Output
|
||
|
||
-- Public interface enforcing invariants
|
||
pattern TIEnum :: forall origin (k :: Kind). () => (k ~ 'Both) => NonEmpty (Definition origin EnumValueInfo) -> TypeInfo origin k
|
||
pattern TIEnum xs <-
|
||
TIEnum__ xs
|
||
where
|
||
TIEnum xs = TIEnum__ (NE.sortWith dName xs)
|
||
|
||
{-# COMPLETE TIScalar, TIEnum, TIInputObject, TIObject, TIInterface, TIUnion #-}
|
||
|
||
instance Eq (TypeInfo origin k) where
|
||
(==) = eqTypeInfo
|
||
|
||
-- | Like '==', but can compare 'TypeInfo's of different kinds.
|
||
eqTypeInfo :: TypeInfo origin k1 -> TypeInfo origin k2 -> Bool
|
||
eqTypeInfo TIScalar TIScalar = True
|
||
eqTypeInfo (TIEnum values1) (TIEnum values2) = values1 == values2
|
||
-- NB the case for input objects currently has quadratic complexity, which is
|
||
-- probably avoidable. HashSets should be able to get this down to
|
||
-- O(n*log(n)). But this requires writing some Hashable instances by hand
|
||
-- because we use some existential types and GADTs.
|
||
eqTypeInfo (TIInputObject ioi1) (TIInputObject ioi2) = ioi1 == ioi2
|
||
eqTypeInfo (TIObject oi1) (TIObject oi2) = oi1 == oi2
|
||
eqTypeInfo (TIInterface ii1) (TIInterface ii2) = ii1 == ii2
|
||
eqTypeInfo (TIUnion (UnionInfo objects1)) (TIUnion (UnionInfo objects2)) =
|
||
eqByName objects1 objects2
|
||
eqTypeInfo _ _ = False
|
||
|
||
getTypeInfo :: Type origin k -> Definition origin (TypeInfo origin k)
|
||
getTypeInfo (TNamed _ d) = d
|
||
getTypeInfo (TList _ t) = getTypeInfo t
|
||
|
||
getObjectInfo :: Type origin k -> Maybe (Definition origin (ObjectInfo origin))
|
||
getObjectInfo t = case getTypeInfo t of
|
||
d@Definition {dInfo = TIObject oi} -> Just d {dInfo = oi}
|
||
_ -> Nothing
|
||
|
||
getInterfaceInfo :: Type origin k -> Maybe (Definition origin (InterfaceInfo origin))
|
||
getInterfaceInfo t = case getTypeInfo t of
|
||
d@Definition {dInfo = TIInterface ii} -> Just d {dInfo = ii}
|
||
_ -> Nothing
|
||
|
||
data SomeDefinitionTypeInfo origin = forall k. SomeDefinitionTypeInfo (Definition origin (TypeInfo origin k))
|
||
|
||
instance HasName (SomeDefinitionTypeInfo origin) where
|
||
getName (SomeDefinitionTypeInfo (Definition n _ _ _)) = n
|
||
|
||
instance Eq (SomeDefinitionTypeInfo origin) where
|
||
-- Same as instance Eq Definition
|
||
SomeDefinitionTypeInfo (Definition name1 _ _ ti1)
|
||
== SomeDefinitionTypeInfo (Definition name2 _ _ ti2) =
|
||
name1 == name2 && eqTypeInfo ti1 ti2
|
||
|
||
data Definition origin a = Definition
|
||
{ dName :: Name,
|
||
dDescription :: Maybe Description,
|
||
-- | What piece of metadata was this fragment of GraphQL type information
|
||
-- from? See also 'Hasura.GraphQL.Schema.Parser'.
|
||
--
|
||
-- 'Nothing' can represent a couple of scenarios:
|
||
-- 1. This is a native part of the GraphQL spec, e.g. the '__Type'
|
||
-- introspection type
|
||
-- 2. This is a native part of HGE, e.g. our scalar types and Relay-related
|
||
-- types
|
||
-- 3. We don't have a clear origin, because
|
||
-- a. Semantically there is no clear origin because it arose from the
|
||
-- combination of several things
|
||
-- b. We generated this 'Definition' in a context where origin
|
||
-- information was no longer in scope
|
||
--
|
||
-- Maybe, at some point, it makes sense to represent the above options more
|
||
-- accurately in the type of 'dOrigin'.
|
||
dOrigin :: Maybe origin,
|
||
-- | Lazy to allow mutually-recursive type definitions.
|
||
dInfo :: ~a
|
||
}
|
||
deriving (Functor, Foldable, Traversable, Generic)
|
||
|
||
instance Hashable a => Hashable (Definition origin a) where
|
||
hashWithSalt salt Definition {..} =
|
||
salt `hashWithSalt` dName `hashWithSalt` dInfo
|
||
|
||
instance Eq a => Eq (Definition origin a) where
|
||
(==) = eq1
|
||
|
||
instance Eq1 (Definition origin) where
|
||
liftEq
|
||
eq
|
||
(Definition name1 _ _ info1)
|
||
(Definition name2 _ _ info2) =
|
||
name1 == name2 && eq info1 info2
|
||
|
||
instance HasName (Definition origin a) where
|
||
getName = dName
|
||
|
||
-- | equivalent to, but faster than...
|
||
--
|
||
-- > map dName x == map dName y
|
||
eqByName :: [Definition origin a] -> [Definition origin a] -> Bool
|
||
eqByName = liftEq ((==) `on` dName)
|
||
|
||
-- | Enum values have no extra information except for the information common to
|
||
-- all definitions, so this is just a placeholder for use as @'Definition'
|
||
-- 'EnumValueInfo'@.
|
||
data EnumValueInfo = EnumValueInfo
|
||
deriving (Eq, Generic)
|
||
|
||
instance Hashable EnumValueInfo
|
||
|
||
data InputFieldInfo origin
|
||
= -- | An input field with a type and possibly a default value. If a default
|
||
-- value is provided, it should be a valid value for the type.
|
||
--
|
||
-- Note that a default value of 'VNull' is subtly different from having no
|
||
-- default value at all. If no default value is provided (i.e. 'Nothing'),
|
||
-- the GraphQL specification allows distinguishing provided @null@ values
|
||
-- from values left completely absent; see Note [The value of omitted
|
||
-- fields] in Hasura.GraphQL.Parser.Internal.Parser.
|
||
forall k. ('Input <: k) => InputFieldInfo (Type origin k) (Maybe (Value Void))
|
||
|
||
instance Eq (InputFieldInfo origin) where
|
||
InputFieldInfo t1 v1 == InputFieldInfo t2 v2 = eqType t1 t2 && v1 == v2
|
||
|
||
data FieldInfo origin = forall k.
|
||
('Output <: k) =>
|
||
FieldInfo
|
||
{ fArguments :: [Definition origin (InputFieldInfo origin)],
|
||
fType :: Type origin k
|
||
}
|
||
|
||
instance Eq (FieldInfo origin) where
|
||
FieldInfo args1 t1 == FieldInfo args2 t2 = args1 == args2 && eqType t1 t2
|
||
|
||
-- -----------------------------------------------------------------------------
|
||
-- support for introspection queries
|
||
|
||
-- | This type represents the directives information to be served over GraphQL introspection
|
||
data DirectiveInfo origin = DirectiveInfo
|
||
{ diName :: !Name,
|
||
diDescription :: !(Maybe Description),
|
||
diArguments :: ![Definition origin (InputFieldInfo origin)],
|
||
diLocations :: ![DirectiveLocation]
|
||
}
|
||
|
||
-- | This type contains all the information needed to efficiently serve GraphQL
|
||
-- introspection queries. It corresponds to the GraphQL @__Schema@ type defined
|
||
-- in <§ 4.5 Schema Introspection http://spec.graphql.org/June2018/#sec-Introspection>.
|
||
-- See also Note [Basics of introspection schema generation].
|
||
data Schema origin = Schema
|
||
{ sDescription :: Maybe Description,
|
||
sTypes :: HashMap Name (SomeDefinitionTypeInfo origin),
|
||
sQueryType :: Type origin 'Output,
|
||
sMutationType :: Maybe (Type origin 'Output),
|
||
sSubscriptionType :: Maybe (Type origin 'Output),
|
||
sDirectives :: [DirectiveInfo origin]
|
||
}
|
||
|
||
data TypeDefinitionsWrapper origin where
|
||
TypeDefinitionsWrapper :: HasTypeDefinitions origin a => a -> TypeDefinitionsWrapper origin
|
||
|
||
{-
|
||
Note [Collecting types from the GraphQL schema]
|
||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
A `Parser` object consists of two things:
|
||
|
||
- a function that is used to process (part of) an incoming query, and
|
||
- a piece of GraphQL type information.
|
||
|
||
The main reason that the GraphQL type information is included is so that we can
|
||
generate responses for the introspection fields `__type` and `__schema`. In
|
||
particular, this requires us to have a complete list of all types being used in
|
||
our schema.
|
||
|
||
When we build our schema, we therefore finish by making a full walk over the
|
||
entirety of the schema, collecting the GraphQL types encountered in a `HashMap`,
|
||
allowing us to look up GraphQL types by name. At this point we might figure out
|
||
that a single name is used to represent two GraphQL types that are materially
|
||
distinct. For instance, the name `author` might be used as both a GraphQL
|
||
object, representing a database table, and as a scalar, e.g. as a string name.
|
||
It also prevents us from having both
|
||
|
||
```
|
||
type author {
|
||
id : int
|
||
name : string
|
||
}
|
||
```
|
||
|
||
and
|
||
|
||
```
|
||
type author {
|
||
id : int
|
||
name : string
|
||
email : string
|
||
}
|
||
```
|
||
|
||
in the schema, as the latter has an additional field and is thus distinct from
|
||
the former, even though it has the same name.
|
||
|
||
In fact, for HGE internally, such name clashes are not problematic. We would
|
||
merely end up exposing illegal introspection results. But in order to produce
|
||
introspection results, we have to explore the GraphQL schema anyway, to collect
|
||
all types. We use this opportunity to help our users figure out whether there
|
||
are any name clashes, and if so what caused them. So we do some work to track
|
||
where in the schema various GraphQL type names were encountered. This type
|
||
collision information is stored in `ConflictingDefinitions`.
|
||
|
||
A typical way in which conflicting type definitions occur in practice is if one
|
||
version of HGE adds a different version of HGE as a remote schema, particularly
|
||
when support for database features was added in the meantime. For instance, if
|
||
we'd add a new operator to boolean expressions, e.g. `XOR`, then this would end
|
||
up adding an additional field to every `<column>_bool_exp` object in our schema,
|
||
which clashes with the old `<column>_bool_exp` that's part of the remote schema.
|
||
This is not a bug of the "conflicting type definitions" logic but a limitation
|
||
of the design of HGE, which would be resolved by e.g. having namespaces for
|
||
different data sources.
|
||
-}
|
||
|
||
-- | Recursively collects all type definitions accessible from the given value,
|
||
-- attempting to detect any conflicting defintions that may have made it this
|
||
-- far (See 'ConflictingDefinitions' for details).
|
||
collectTypeDefinitions ::
|
||
HasTypeDefinitions origin a =>
|
||
a ->
|
||
Either (ConflictingDefinitions origin) (HashMap Name (SomeDefinitionTypeInfo origin))
|
||
collectTypeDefinitions x =
|
||
fmap (fmap fst) $
|
||
runExcept $
|
||
flip execStateT Map.empty $
|
||
flip runReaderT (TypeOriginStack []) $
|
||
runTypeAccumulation $
|
||
accumulateTypeDefinitions x
|
||
|
||
-- | A path through 'Definition', accumulated in 'accumulateTypeDefinitions'
|
||
-- only to power 'ConflictingDefinitions' in the error case.
|
||
newtype TypeOriginStack = TypeOriginStack [Name]
|
||
|
||
-- Add the current field name to the origin stack
|
||
typeOriginRecurse :: Name -> TypeOriginStack -> TypeOriginStack
|
||
typeOriginRecurse field (TypeOriginStack origins) = TypeOriginStack (field : origins)
|
||
|
||
-- This is kind of a hack to make sure that the query root name is part of the origin stack
|
||
typeRootRecurse :: Name -> TypeOriginStack -> TypeOriginStack
|
||
typeRootRecurse rootName (TypeOriginStack []) = (TypeOriginStack [rootName])
|
||
typeRootRecurse _ x = x
|
||
|
||
instance ToTxt TypeOriginStack where
|
||
toTxt (TypeOriginStack fields) = T.intercalate "." $ toTxt <$> reverse fields
|
||
|
||
-- | NOTE: it's not clear exactly where we'd get conflicting definitions at the
|
||
-- point 'collectTypeDefinitions' is called, but conflicting names from
|
||
-- different data sources is apparently one place (TODO some tests that
|
||
-- excercise this).
|
||
--
|
||
-- ALSO NOTE: it's difficult to see in isolation how or if this check is
|
||
-- correct since 'Definition' is cyclic and has no accomodations for observable
|
||
-- sharing (formerly it had Uniques; see commit history and discussion in
|
||
-- #3685). The check relies on dodgy Eq instances for the types that make up
|
||
-- the Definition graph (see e.g. @instance Eq ObjectInfo@).
|
||
--
|
||
-- See Note [Collecting types from the GraphQL schema]
|
||
data ConflictingDefinitions origin
|
||
= -- | Type collection has found at least two types with the same name.
|
||
ConflictingDefinitions
|
||
(SomeDefinitionTypeInfo origin, TypeOriginStack)
|
||
(SomeDefinitionTypeInfo origin, NonEmpty TypeOriginStack)
|
||
|
||
-- | Although the majority of graphql-engine is written in terms of abstract
|
||
-- mtl-style effect monads, we figured out that this particular codepath is
|
||
-- quite hot, and that mtl has a measurable negative effect for accumulating
|
||
-- types from the schema, both in profiling and in benchmarking. Using an
|
||
-- explicit transformers-style effect stack seems to overall memory usage by
|
||
-- about 3-7%.
|
||
newtype TypeAccumulation origin a = TypeAccumulation
|
||
{ runTypeAccumulation ::
|
||
ReaderT
|
||
TypeOriginStack
|
||
( StateT
|
||
(HashMap Name (SomeDefinitionTypeInfo origin, NonEmpty TypeOriginStack))
|
||
(ExceptT (ConflictingDefinitions origin) Identity)
|
||
)
|
||
a
|
||
}
|
||
deriving (Functor, Applicative, Monad)
|
||
deriving (MonadReader TypeOriginStack)
|
||
deriving (MonadState (HashMap Name (SomeDefinitionTypeInfo origin, NonEmpty TypeOriginStack)))
|
||
deriving (MonadError (ConflictingDefinitions origin))
|
||
|
||
class HasTypeDefinitions origin a where
|
||
-- | Recursively accumulates all type definitions accessible from the given
|
||
-- value. This is done statefully to avoid infinite loops arising from
|
||
-- recursive type definitions; see Note [Tying the knot] in Hasura.GraphQL.Parser.Class.
|
||
accumulateTypeDefinitions ::
|
||
a -> TypeAccumulation origin ()
|
||
|
||
instance HasTypeDefinitions origin (Definition origin (TypeInfo origin k)) where
|
||
accumulateTypeDefinitions new@Definition {..} = do
|
||
-- This is the important case! We actually have a type definition, so we
|
||
-- need to add it to the state.
|
||
definitions <- get
|
||
stack <- ask
|
||
let someNew = SomeDefinitionTypeInfo new
|
||
case Map.lookup dName definitions of
|
||
Nothing -> do
|
||
put $! Map.insert dName (someNew, pure stack) definitions
|
||
-- This type definition might reference other type definitions, so we
|
||
-- still need to recur.
|
||
local (typeRootRecurse dName) $ accumulateTypeDefinitions dInfo
|
||
Just (someOld, origins)
|
||
-- It’s important we /don’t/ recur if we’ve already seen this definition
|
||
-- before to avoid infinite loops; see Note [Tying the knot] in Hasura.GraphQL.Parser.Class.
|
||
-- (NOTE: I tried making `origins` an STRef and doing a mutable update
|
||
-- here but the performance was about the same)
|
||
| someOld == someNew -> put $! Map.insert dName (someOld, stack `NE.cons` origins) definitions
|
||
| otherwise -> throwError $ ConflictingDefinitions (someNew, stack) (someOld, origins)
|
||
|
||
instance HasTypeDefinitions origin a => HasTypeDefinitions origin [a] where
|
||
accumulateTypeDefinitions = traverse_ accumulateTypeDefinitions
|
||
|
||
instance HasTypeDefinitions origin a => HasTypeDefinitions origin (Maybe a) where
|
||
accumulateTypeDefinitions = traverse_ accumulateTypeDefinitions
|
||
|
||
instance HasTypeDefinitions origin (TypeDefinitionsWrapper origin) where
|
||
accumulateTypeDefinitions (TypeDefinitionsWrapper x) = accumulateTypeDefinitions x
|
||
|
||
instance HasTypeDefinitions origin (Type origin k) where
|
||
accumulateTypeDefinitions = \case
|
||
TNamed _ t -> accumulateTypeDefinitions t
|
||
TList _ t -> accumulateTypeDefinitions t
|
||
|
||
instance HasTypeDefinitions origin (TypeInfo origin k) where
|
||
accumulateTypeDefinitions = \case
|
||
TIScalar -> pure ()
|
||
TIEnum _ -> pure ()
|
||
TIInputObject (InputObjectInfo fields) -> accumulateTypeDefinitions fields
|
||
TIObject (ObjectInfo fields interfaces) ->
|
||
accumulateTypeDefinitions fields >> accumulateTypeDefinitions interfaces
|
||
TIInterface (InterfaceInfo fields objects) ->
|
||
accumulateTypeDefinitions fields
|
||
>> accumulateTypeDefinitions objects
|
||
TIUnion (UnionInfo objects) -> accumulateTypeDefinitions objects
|
||
|
||
instance HasTypeDefinitions origin (Definition origin (InputObjectInfo origin)) where
|
||
accumulateTypeDefinitions = accumulateTypeDefinitions . fmap TIInputObject
|
||
|
||
instance HasTypeDefinitions origin (Definition origin (InputFieldInfo origin)) where
|
||
accumulateTypeDefinitions Definition {..} =
|
||
local (typeOriginRecurse dName) $ accumulateTypeDefinitions dInfo
|
||
|
||
instance HasTypeDefinitions origin (InputFieldInfo origin) where
|
||
accumulateTypeDefinitions (InputFieldInfo t _) =
|
||
accumulateTypeDefinitions t
|
||
|
||
instance HasTypeDefinitions origin (Definition origin (FieldInfo origin)) where
|
||
accumulateTypeDefinitions Definition {..} =
|
||
local (typeOriginRecurse dName) $ accumulateTypeDefinitions dInfo
|
||
|
||
instance HasTypeDefinitions origin (FieldInfo origin) where
|
||
accumulateTypeDefinitions (FieldInfo args t) = do
|
||
accumulateTypeDefinitions args
|
||
accumulateTypeDefinitions t
|
||
|
||
instance HasTypeDefinitions origin (Definition origin (ObjectInfo origin)) where
|
||
accumulateTypeDefinitions d@Definition {..} =
|
||
local (typeOriginRecurse dName) $ accumulateTypeDefinitions (fmap TIObject d)
|
||
|
||
instance HasTypeDefinitions origin (Definition origin (InterfaceInfo origin)) where
|
||
accumulateTypeDefinitions d@Definition {..} =
|
||
local (typeOriginRecurse dName) $ accumulateTypeDefinitions (fmap TIInterface d)
|
||
|
||
instance HasTypeDefinitions origin (Definition origin (UnionInfo origin)) where
|
||
accumulateTypeDefinitions d@Definition {..} =
|
||
local (typeOriginRecurse dName) $ accumulateTypeDefinitions (fmap TIUnion d)
|
||
|
||
{- PERFORMANCE NOTE/TODO:
|
||
|
||
Since Definition's are cyclic I spent a little time trying to optimize the
|
||
== in accumulateTypeDefinitions into a nearly-noop using pointer
|
||
equality, but could not get it to trigger unless I called it on the unlifted
|
||
ByteArray# within dName, but at that point what was a pretty small theoretical
|
||
benefit disappeared for whatever reason (plus wasn't strictly safe at that
|
||
point). (Note, to have any luck calling on Definitions directly we would need
|
||
to fix the reallocation of Definitions via @fmap TI...@ in
|
||
accumulateTypeDefinitions as well)
|
||
|
||
The TODO-flavored thing here is to investigate whether we might not have as
|
||
much sharing here as we assume. We can use ghc-debug to inspect the object in
|
||
the heap.
|
||
|
||
We might also then rewrite accumulateTypeDefinitions to return non-cyclic type
|
||
definition segmants corresponding to the equality logic here (see "dodgy"
|
||
equality comments), and even consider trying to do some kind of global
|
||
interning of these across roles (though I think that would only be an
|
||
very incremental improvement...)
|
||
|
||
-- | See e.g. https://github.com/haskell/containers/blob/master/containers/src/Utils/Containers/Internal/PtrEquality.hs
|
||
--
|
||
-- If this returns True then the arguments are equal (for any sane definition of equality)
|
||
-- if this returns False nothing can be determined. The caller must ensure
|
||
-- referential transparency is preserved...
|
||
unsafeHetPtrEq :: a -> b -> Bool
|
||
unsafeHetPtrEq !x !y = isTrue# (unsafeCoerce (reallyUnsafePtrEquality# :: x -> x -> Int#) x y)
|
||
{-# INLINE unsafeHetPtrEq #-}
|
||
infix 4 `unsafeHetPtrEq` -- just like (==)
|
||
|
||
-- | Equivalent to @(==)@ but potentially faster in cases where the arguments
|
||
-- might be pointer-identical.
|
||
fastEq :: (Eq a)=> a -> a -> Bool
|
||
fastEq !x !y =
|
||
-- See e.g. https://github.com/haskell/containers/blob/master/containers/src/Utils/Containers/Internal/PtrEquality.hs
|
||
isTrue# (reallyUnsafePtrEquality# x y) || x == y
|
||
|
||
infix 4 `fastEq` -- just like (==)
|
||
-}
|