Skip to content

Commit 67b35c7

Browse files
committed
Support __typename introspection
1 parent 377c332 commit 67b35c7

File tree

4 files changed

+102
-18
lines changed

4 files changed

+102
-18
lines changed

graphql-api.cabal

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
--
33
-- see: https://github.com/sol/hpack
44
--
5-
-- hash: 6db006b020fe198ac64b8a50f8335017251389b7c34dfc553675e38eb001a428
5+
-- hash: 24bc26dbd1f77e90690a71683ae20372d13f4729b6073940a17679e5bc18c609
66

77
name: graphql-api
88
version: 0.3.0
@@ -133,6 +133,7 @@ test-suite graphql-api-tests
133133
build-depends:
134134
QuickCheck
135135
, aeson
136+
, aeson-qq
136137
, attoparsec
137138
, base >=4.9 && <5
138139
, containers
@@ -144,6 +145,7 @@ test-suite graphql-api-tests
144145
, raw-strings-qq
145146
, tasty
146147
, tasty-hspec
148+
, text
147149
, transformers
148150
other-modules:
149151
ASTTests

package.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,10 @@ tests:
7171
- hspec
7272
- QuickCheck
7373
- raw-strings-qq
74+
- aeson-qq
7475
- tasty
7576
- tasty-hspec
77+
- text
7678
- directory
7779

7880
graphql-api-doctests:

src/GraphQL/Internal/Resolver.hs

+25-16
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,9 @@ import GraphQL.Value
5353
, FromValue(..)
5454
, ToValue(..)
5555
)
56-
import GraphQL.Internal.Name (Name, HasName(..))
56+
import GraphQL.Internal.Name (Name, HasName(..), unName)
5757
import qualified GraphQL.Internal.OrderedMap as OrderedMap
58+
import GraphQL.Internal.Schema (ObjectTypeDefinition(..))
5859
import GraphQL.Internal.Output (GraphQLError(..))
5960
import GraphQL.Internal.Validation
6061
( SelectionSetByType
@@ -212,9 +213,16 @@ type family FieldName (a :: Type) = (r :: Symbol) where
212213
FieldName x = TypeError ('Text "Unexpected branch in FieldName type family. Please file a bug!" ':<>: 'ShowType x)
213214

214215
resolveField :: forall dispatchType (m :: Type -> Type).
215-
(BuildFieldResolver m dispatchType, Monad m, KnownSymbol (FieldName dispatchType))
216-
=> FieldHandler m dispatchType -> m ResolveFieldResult -> Field Value -> m ResolveFieldResult
217-
resolveField handler nextHandler field =
216+
( BuildFieldResolver m dispatchType
217+
, Monad m
218+
, KnownSymbol (FieldName dispatchType)
219+
)
220+
=> FieldHandler m dispatchType
221+
-> m ResolveFieldResult
222+
-> ObjectTypeDefinition
223+
-> Field Value
224+
-> m ResolveFieldResult
225+
resolveField handler nextHandler defn field =
218226
-- check name before
219227
case API.nameFromSymbol @(FieldName dispatchType) of
220228
Left err -> pure (Result [SchemaError err] (Just GValue.ValueNull))
@@ -225,6 +233,8 @@ resolveField handler nextHandler field =
225233
Right resolver -> do
226234
Result errs value <- resolver
227235
pure (Result errs (Just value))
236+
| getName field == "__typename" ->
237+
pure $ Result [] (Just $ GValue.ValueString $ GValue.String $ unName $ getName defn)
228238
| otherwise -> nextHandler
229239

230240
-- We're using our usual trick of rewriting a type in a closed type
@@ -312,7 +322,6 @@ type family RunFieldsHandler (m :: Type -> Type) (a :: Type) = (r :: Type) where
312322
RunFieldsHandler m a = TypeError (
313323
'Text "Unexpected RunFieldsHandler types: " ':<>: 'ShowType a)
314324

315-
316325
class RunFields m a where
317326
-- | Run a single 'Selection' over all possible fields (as specified by the
318327
-- type @a@), returning exactly one 'GValue.ObjectField' when a field
@@ -321,7 +330,7 @@ class RunFields m a where
321330
-- Individual implementations are responsible for calling 'runFields' if
322331
-- they haven't matched the field and there are still candidate fields
323332
-- within the handler.
324-
runFields :: RunFieldsHandler m a -> Field Value -> m ResolveFieldResult
333+
runFields :: RunFieldsHandler m a -> ObjectTypeDefinition -> Field Value -> m ResolveFieldResult
325334

326335
instance forall f fs m dispatchType.
327336
( BuildFieldResolver m dispatchType
@@ -330,19 +339,19 @@ instance forall f fs m dispatchType.
330339
, KnownSymbol (FieldName dispatchType)
331340
, Monad m
332341
) => RunFields m (f :<> fs) where
333-
runFields (handler :<> nextHandlers) field =
334-
resolveField @dispatchType @m handler nextHandler field
342+
runFields (handler :<> nextHandlers) defn field =
343+
resolveField @dispatchType @m handler nextHandler defn field
335344
where
336-
nextHandler = runFields @m @fs nextHandlers field
345+
nextHandler = runFields @m @fs nextHandlers defn field
337346

338347
instance forall ksM t m dispatchType.
339348
( BuildFieldResolver m dispatchType
340349
, KnownSymbol ksM
341350
, dispatchType ~ FieldResolverDispatchType (API.Field ksM t)
342351
, Monad m
343352
) => RunFields m (API.Field ksM t) where
344-
runFields handler field =
345-
resolveField @dispatchType @m handler nextHandler field
353+
runFields handler defn field =
354+
resolveField @dispatchType @m handler nextHandler defn field
346355
where
347356
nextHandler = pure (Result [FieldNotFoundError (getName field)] Nothing)
348357

@@ -352,8 +361,8 @@ instance forall m a b dispatchType.
352361
, KnownSymbol (FieldName dispatchType)
353362
, Monad m
354363
) => RunFields m (a :> b) where
355-
runFields handler field =
356-
resolveField @dispatchType @m handler nextHandler field
364+
runFields handler defn field =
365+
resolveField @dispatchType @m handler nextHandler defn field
357366
where
358367
nextHandler = pure (Result [FieldNotFoundError (getName field)] Nothing)
359368

@@ -368,12 +377,12 @@ instance forall typeName interfaces fields m.
368377
resolve mHandler (Just selectionSet) =
369378
case getSelectionSet of
370379
Left err -> throwE err
371-
Right ss -> do
380+
Right (ss, defn) -> do
372381
-- Run the handler so the field resolvers have access to the object.
373382
-- This (and other places, including field resolvers) is where user
374383
-- code can do things like look up something in a database.
375384
handler <- mHandler
376-
r <- traverse (runFields @m @(RunFieldsType m fields) handler) ss
385+
r <- traverse (runFields @m @(RunFieldsType m fields) handler defn) ss
377386
let (Result errs obj) = GValue.objectFromOrderedMap . OrderedMap.catMaybes <$> sequenceA r
378387
pure (Result errs (GValue.ValueObject obj))
379388

@@ -391,7 +400,7 @@ instance forall typeName interfaces fields m.
391400
-- See <https://facebook.github.io/graphql/#sec-Field-Collection> for
392401
-- more details.
393402
(SelectionSet ss') <- first ValidationError $ getSelectionSetForType defn selectionSet
394-
pure ss'
403+
pure (ss', defn)
395404

396405
-- TODO(tom): we're getting to a point where it might make sense to
397406
-- split resolver into submodules (GraphQL.Resolver.Union etc.)

tests/ResolverTests.hs

+72-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE QuasiQuotes #-}
23
{-# LANGUAGE ScopedTypeVariables #-}
34
{-# LANGUAGE TypeOperators #-}
45
module ResolverTests (tests) where
56

67
import Protolude hiding (Enum)
78

9+
import Data.Aeson.QQ (aesonQQ)
10+
import Text.RawString.QQ (r)
811
import Test.Tasty (TestTree)
912
import Test.Tasty.Hspec (testSpec, describe, it, shouldBe)
1013

11-
import Data.Aeson (encode)
14+
import Data.Aeson (encode, toJSON)
1215
import GraphQL
1316
( Response(..)
1417
, interpretAnonymousQuery
@@ -18,12 +21,14 @@ import GraphQL.API
1821
, Field
1922
, Argument
2023
, Enum
24+
, Union
2125
, (:>)
2226
)
2327
import GraphQL.Resolver
2428
( Handler
2529
, ResolverError(..)
2630
, (:<>)(..)
31+
, unionValue
2732
)
2833
import GraphQL.Internal.Output (singleError)
2934

@@ -74,6 +79,28 @@ enumHandler :: Handler IO EnumQuery
7479
enumHandler = pure $ pure NormalFile
7580
-- /Enum test
7681

82+
-- Union test
83+
type Cat = Object "Cat" '[] '[Field "name" Text]
84+
type Dog = Object "Dog" '[] '[Field "name" Text]
85+
type CatOrDog = Union "CatOrDog" '[Cat, Dog]
86+
type UnionQuery = Object "UnionQuery" '[]
87+
'[ Argument "isCat" Bool :> Field "catOrDog" CatOrDog
88+
]
89+
90+
dogHandler :: Handler IO Cat
91+
dogHandler = pure $ pure "Mortgage"
92+
93+
catHandler :: Handler IO Dog
94+
catHandler = pure $ pure "Felix"
95+
96+
unionHandler :: Handler IO UnionQuery
97+
unionHandler = pure $ \isCat ->
98+
if isCat
99+
then unionValue @Cat catHandler
100+
else unionValue @Dog dogHandler
101+
102+
-- /Union test
103+
77104
tests :: IO TestTree
78105
tests = testSpec "TypeAPI" $ do
79106
describe "tTest" $ do
@@ -94,3 +121,47 @@ tests = testSpec "TypeAPI" $ do
94121
it "API.Enum works" $ do
95122
Success object <- interpretAnonymousQuery @EnumQuery enumHandler "{ mode }"
96123
encode object `shouldBe` "{\"mode\":\"NormalFile\"}"
124+
125+
describe "Introspection" $ do
126+
describe "__typename" $ do
127+
it "can describe nested objects" $ do
128+
Success object <- interpretAnonymousQuery @Query handler [r|
129+
{
130+
__typename
131+
test(id: "1") {
132+
__typename
133+
name
134+
}
135+
}
136+
|]
137+
138+
toJSON object `shouldBe` [aesonQQ|
139+
{
140+
"__typename": "Query",
141+
"test": {
142+
"__typename": "Foo",
143+
"name": "Mort"
144+
}
145+
}
146+
|]
147+
148+
it "can describe unions" $ do
149+
Success object <- interpretAnonymousQuery @UnionQuery unionHandler [r|
150+
{
151+
__typename
152+
catOrDog(isCat: false) {
153+
__typename
154+
name
155+
}
156+
}
157+
|]
158+
159+
toJSON object `shouldBe` [aesonQQ|
160+
{
161+
"__typename": "UnionQuery",
162+
"catOrDog": {
163+
"__typename": "Dog",
164+
"name": "Mortgage"
165+
}
166+
}
167+
|]

0 commit comments

Comments
 (0)