git.iko.soy/rowtype-yoga/purescript-graphql
on master help sign in
Restructure packages and export from GraphQL

This commit is a huge restructuring of all the functions in this package. The goal of the restructuring is to make it easier to work with the codebase. Also, almost all values are now exported from the root module.

commit76c05e0f5c000a023c2307d42310cadff2452976 authorhendrikniemann <h.neverman@web.de> dateWed 3 Mar 2021, 21:22 (6 years ago) parent47fcf5d
23 files changed+1496−1076
README.md+5−7 docs/_sidebar.md+5−3 packages.dhall+1−1 src/GraphQL.purs+16−7 src/GraphQL/Type/Introspection.purs → src/GraphQL/Builtin/Introspection.purs+5−4 src/GraphQL/Type/Scalar.purs → src/GraphQL/Builtin/Scalar.purs+1−1 src/GraphQL/DSL.purs+822 src/GraphQL/Execution.purs+4−2 src/GraphQL/Execution/FlattenAst.purs+50 src/GraphQL/Execution/Normalize.purs+42 src/GraphQL/Language/Printer.purs+80 src/GraphQL/Type.purs+17−1023 src/GraphQL/Type/Class.purs+37 src/GraphQL/Type/EnumType.purs+83 src/GraphQL/Type/InputObjectType.purs+49 src/GraphQL/Type/Introspection/Datatypes.purs+1 src/GraphQL/Type/Introspection/Util.purs+7−1 src/GraphQL/Type/ObjectType.purs+128 src/GraphQL/Type/ScalarType.purs+55 src/GraphQL/Type/Schema.purs+8 src/GraphQL/Type/UnionType.purs+57 test/Execution.purs+19−21 test/Readme.purs+4−6
README.md+5−7
@@ -28,15 +28,13 @@ module Main where
2828
2929import Prelude
3030
31−import Data.Argonaut.Core (stringify)
31+import Data.Argonaut (stringify)
3232import Data.Map as Map
3333import Data.Symbol (SProxy(..))
3434import Effect (Effect)
3535import Effect.Console as Console
36−import GraphQL (graphql)
37−import GraphQL.Type ((!>), (.>), (:>), (?>))
38−import GraphQL.Type as GraphQL
39−import GraphQL.Type.Scalar as Scalar
36+import GraphQL ((!>), (.>), (:>), (?>), graphql)
37+import GraphQL as GraphQL
4038
4139main :: Effect Unit
4240main = do
@@ -50,8 +48,8 @@ queryType :: GraphQL.ObjectType Effect Unit
5048queryType =
5149 GraphQL.objectType "Query"
5250 .> "The root query type."
53− :> GraphQL.field "hello" Scalar.string
54− ?> GraphQL.arg Scalar.string (SProxy :: SProxy "name")
51+ :> GraphQL.field "hello" GraphQL.string
52+ ?> GraphQL.arg GraphQL.string (SProxy :: SProxy "name")
5553 .> "A simple field that returns a greeting."
5654 !> (\{ name } _ -> pure $ "Hello, " <> name <> "!")
5755```
docs/_sidebar.md+5−3
@@ -1,8 +1,10 @@
1+- [Introduction](README.md)
2+- [What's new in v2?](version_2.md)
13- Tutorial
2−
3− - [Introduction](README.md)
4− - [What's new in v2?](version_2.md)
54 - [Intro To GraphQL](intro_to_graphql.md)
65 - [Getting Started](getting_started.md)
76 - [Building the HTTP server](http_server.md)
7+- Reference
8+ - [Scalar Types](scalar_types.md)
9+ - [Object Types](object_types.md)
810 - [Union Types and Interfaces](union_types.md)
packages.dhall+1−1
@@ -1,5 +1,5 @@
11let upstream =
2− https://github.com/purescript/package-sets/releases/download/psc-0.13.3-20191005/packages.dhall sha256:ba287d858ada09c4164792ad4e643013b742c208cbedf5de2e35ee27b64b6817
2+ https://github.com/purescript/package-sets/releases/download/psc-0.13.8-20200922/packages.dhall sha256:5edc9af74593eab8834d7e324e5868a3d258bbab75c5531d2eb770d4324a2900
33
44let overrides = {=}
55
src/GraphQL.purs+16−7
@@ -1,4 +1,11 @@
1−module GraphQL where
1+module GraphQL
2+ ( graphql
3+ , module Execution
4+ , module Language
5+ , module GQL
6+ , module DSL
7+ , module Scalar
8+ ) where
29
310import Prelude
411
@@ -10,10 +17,12 @@ import Data.Map (Map, fromFoldable)
1017import Data.Maybe (Maybe(..))
1118import Data.Tuple (Tuple(..))
1219import Effect.Exception (Error, error)
13−import GraphQL.Execution (execute)
14−import GraphQL.Language (parse)
20+import GraphQL.DSL (class ArgsDefToArgsParam, class ArgsFromRows, class Describe, class UnionDefinition, class UnionIntrospection, class UnionResolver, arg, argsFromDefinition, argsFromRows, describe, enumType, field, inputField, optionalInputField, inputObjectType, listField, nullableField, nullableListField, objectType, optionalArg, union, unionIntrospection, unionResolver, withArgument, withField, withInputField, withMappingResolver, withResolver, withSimpleResolver, (!!>), (!#>), (!>), (.>), (:>), (:?>), (?>)) as DSL
21+import GraphQL.Execution (execute) as Execution
22+import GraphQL.Language (parse) as Language
1523import GraphQL.Language.AST as AST
16−import GraphQL.Type (Schema)
24+import GraphQL.Type (class GraphQLType, class InputType, class OutputType, Argument, EnumType, ExecutionContext, Field, InputField(..), InputObjectType(..), ObjectType, ScalarType(..), Schema(..), UnionType, input, introspect, output) as GQL
25+import GraphQL.Builtin.Scalar (boolean, float, id, int, string) as Scalar
1726
1827-- | Parses a GraphQL query string into a document and then executes the query given the parameters
1928-- |
@@ -28,14 +37,14 @@ import GraphQL.Type (Schema)
2837graphql ::
2938 ∀ m a.
3039 MonadError Error m =>
31− Schema m a ->
40+ GQL.Schema m a ->
3241 String ->
3342 Map String Json ->
3443 Maybe String ->
3544 a ->
3645 m Json
3746graphql schema query variables operation root =
38− case parse query of
47+ case Language.parse query of
3948 Left message -> throwError $ error $ "Parsing Error: " <> message
4049 Right document@(AST.DocumentNode { definitions }) ->
4150 let
@@ -43,4 +52,4 @@ graphql schema query variables operation root =
4352 f@(AST.FragmentDefinitionNode { name: AST.NameNode n }) -> pure $ Tuple n.value f
4453 _ -> Nothing
4554 in
46− execute document schema { variables, fragments } operation (pure root)
55+ Execution.execute document schema { variables, fragments } operation (pure root)
src/GraphQL/Type/Introspection.purs → src/GraphQL/Builtin/Introspection.pursrenamed+5−4
@@ -1,4 +1,4 @@
1−module GraphQL.Type.Introspection where
1+module GraphQL.Builtin.Introspection where
22
33import Prelude
44
@@ -8,11 +8,12 @@ import Data.Array (fromFoldable)
88import Data.Maybe (Maybe(..), isJust)
99import Data.Newtype (unwrap)
1010import Effect.Exception (Error)
11−import GraphQL.Type ((!#>), (.>), (:>))
12−import GraphQL.Type as GQL
11+import GraphQL.DSL ((!#>), (.>), (:>))
12+import GraphQL.DSL (enumType, field, listField, nullableField, nullableListField, objectType) as GQL
13+import GraphQL.Type (ObjectType, EnumType) as GQL
1314import GraphQL.Type.Introspection.Datatypes (EnumValueIntrospection, FieldIntrospection, InputValueIntrospection, SchemaIntrospection, TypeIntrospection(..), TypeKind, getDescription, getName, getTypeKind)
1415import GraphQL.Type.Introspection.Util (collectTypes)
15−import GraphQL.Type.Scalar as Scalar
16+import GraphQL.Builtin.Scalar as Scalar
1617
1718
1819schemaType :: forall m. (MonadError Error m) => GQL.ObjectType m SchemaIntrospection
src/GraphQL/Type/Scalar.purs → src/GraphQL/Builtin/Scalar.pursrenamed+1−1
@@ -1,4 +1,4 @@
1−module GraphQL.Type.Scalar where
1+module GraphQL.Builtin.Scalar where
22
33import Prelude
44
src/GraphQL/DSL.pursnew file+822
@@ -0,0 +1,822 @@
1+module GraphQL.DSL where
2+
3+import Prelude
4+
5+import Control.Monad.Error.Class (class MonadError, throwError)
6+import Data.Argonaut (printJsonDecodeError)
7+import Data.Argonaut as Json
8+import Data.Array (find)
9+import Data.Array as Array
10+import Data.Bifunctor (lmap)
11+import Data.Either (Either(..), note)
12+import Data.Enum (class Enum, enumFromTo)
13+import Data.List (List, fromFoldable)
14+import Data.Map (empty, insert, lookup)
15+import Data.Maybe (Maybe(..), fromMaybe)
16+import Data.Newtype (unwrap)
17+import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol)
18+import Data.Traversable (class Traversable, traverse)
19+import Data.Tuple (Tuple(..))
20+import Data.Variant as Variant
21+import Effect.Exception (Error, error)
22+import GraphQL.Execution.Result (Result(..))
23+import GraphQL.Language.AST as AST
24+import GraphQL.Type.Class (class InputType, class OutputType, ExecutionContext, input, introspect, output)
25+import GraphQL.Type.EnumType (EnumType(..), EnumValue(..))
26+import GraphQL.Type.InputObjectType (InputField(..), InputObjectType(..))
27+import GraphQL.Type.Introspection.Datatypes as IntrospectionTypes
28+import GraphQL.Type.ObjectType (Argument(..), ExecutableField(..), Field(..), ObjectType(..))
29+import GraphQL.Type.UnionType (UnionType(..), filterSelectionSet)
30+import Record as Record
31+import Type.Data.RowList (RLProxy(..))
32+import Type.Row as Row
33+import Type.RowList as RL
34+import Unsafe.Coerce (unsafeCoerce)
35+
36+
37+-- | Creates an empty object type with the given name. Fields can be added to the object type using
38+-- | the `:>` operator from this module and the `field` functions. Object types should have at least
39+-- | one field to confirm to the GraphQL specification.
40+-- |
41+-- | *Example:*
42+-- | ```purescript
43+-- | queryType :: ObjectType Unit
44+-- | queryType = objectType "Query"
45+-- | ```
46+objectType :: forall m a. String -> ObjectType m a
47+objectType name =
48+ let
49+ introspection = IntrospectionTypes.ObjectTypeIntrospection
50+ { name, description: Nothing, fields: [] }
51+ in
52+ ObjectType (\_ -> { name, fields: empty, introspection })
53+
54+
55+-- | Create a new field for an object type. This function is typically used together with the
56+-- | `:>` operator that automatically converts the field into an executable field.
57+field :: forall m t a. OutputType m t => MonadError Error m => String -> t a -> Field m a () ()
58+field name t =
59+ Field
60+ { name
61+ , description: Nothing
62+ , args: {}
63+ , serialize
64+ , argumentIntrospections: []
65+ , typeIntrospection }
66+ where
67+ serialize (AST.FieldNode node) execCtx _ val = output t node.selectionSet execCtx (pure val)
68+ serialize _ _ _ _ = pure $ ResultError "Obtained non FieldNode for field serialisation."
69+
70+ typeIntrospection _ =
71+ IntrospectionTypes.NonNullTypeIntrospection { ofType: \_ -> introspect t }
72+
73+
74+-- | Create a new field for an object type that is a list. You can return any `Traversable` from the
75+-- | resolver.
76+listField :: forall m f t a.
77+ MonadError Error m =>
78+ Traversable f =>
79+ OutputType m t =>
80+ String ->
81+ t a ->
82+ Field m (f a) () ()
83+listField name t =
84+ Field
85+ { name
86+ , description: Nothing
87+ , args: {}
88+ , serialize
89+ , argumentIntrospections: []
90+ , typeIntrospection }
91+ where
92+ serialize (AST.FieldNode node) execCtx _ vals = do
93+ ResultList <$> fromFoldable <$> traverse (output t node.selectionSet execCtx) (pure <$> vals)
94+
95+ serialize _ _ _ _ =
96+ pure $ ResultError "Obtained non FieldNode for field serialisation."
97+
98+ typeIntrospection _ =
99+ IntrospectionTypes.NonNullTypeIntrospection
100+ { ofType: \_ ->
101+ IntrospectionTypes.ListTypeIntrospection { ofType: \_ -> introspect t }
102+ }
103+
104+
105+-- | Create a new field for an object type that is optional (i.e. it can be null). The resolver
106+-- | must now return a `Maybe`.
107+nullableField :: forall m t a.
108+ MonadError Error m =>
109+ OutputType m t =>
110+ String ->
111+ t a ->
112+ Field m (Maybe a) () ()
113+nullableField name t =
114+ Field
115+ { name
116+ , description: Nothing
117+ , args: {}
118+ , serialize
119+ , argumentIntrospections: []
120+ , typeIntrospection }
121+ where
122+ serialize (AST.FieldNode node) execCtx _ value = do
123+ case value of
124+ Nothing ->
125+ pure $ ResultNullable Nothing
126+
127+ Just val ->
128+ ResultNullable <$> Just <$> output t node.selectionSet execCtx (pure val)
129+
130+ serialize _ _ _ _ =
131+ pure $ ResultError "Obtained non FieldNode for field serialisation."
132+
133+ typeIntrospection _ = introspect t
134+
135+
136+-- | Create a new field for an object type that is optional (i.e. it can be null) and returns a
137+-- | list. The resolver must now return a `Maybe`.
138+nullableListField :: forall m f t a.
139+ MonadError Error m =>
140+ Traversable f =>
141+ OutputType m t =>
142+ String ->
143+ t a ->
144+ Field m (Maybe (f a)) () ()
145+nullableListField name t =
146+ Field
147+ { name
148+ , description: Nothing
149+ , args: {}
150+ , serialize
151+ , argumentIntrospections: []
152+ , typeIntrospection }
153+ where
154+ serialize (AST.FieldNode node) variables _ values = do
155+ case values of
156+ Nothing ->
157+ pure $ ResultNullable Nothing
158+
159+ Just vals ->
160+ ResultNullable <$>
161+ Just <$>
162+ ResultList <$>
163+ fromFoldable <$>
164+ traverse (output t node.selectionSet variables) (pure <$> vals)
165+ serialize _ _ _ _ =
166+ pure $ ResultError "Obtained non FieldNode for field serialisation."
167+
168+ typeIntrospection _ =
169+ IntrospectionTypes.ListTypeIntrospection { ofType: \_ -> introspect t }
170+
171+
172+-- | Create a tuple with a given name and a plain argument of the given type. Arguments are passed
173+-- | into the resolver. Arguments created with this function are required and the query fails if
174+-- | no value or a `null` value is supplied for this argument. To make an argument optional use
175+-- | `optionalArg` instead.
176+-- | The name argument tuple can then be used to be added to a field using the `?>` operator.
177+-- |
178+-- | *Example:*
179+-- | ```purescript
180+-- | objectType "Query"
181+-- | :> field "hello" Scalar.string
182+-- | ?> arg Scalar.string (SProxy :: _ "name")
183+-- | !> \{ name } parent -> pure $ "Hello " <> name
184+-- | ```
185+arg :: forall t a n. InputType t => IsSymbol n => t a -> SProxy n -> Tuple (SProxy n) (Argument a)
186+arg t name =
187+ Tuple (SProxy :: _ n) $
188+ Argument
189+ { description: Nothing
190+ , resolveValue: input t
191+ , typeIntrospection: \_ -> introspect t
192+ }
193+
194+
195+-- | Create a tuple with a given name and a plain argument of the given type, that is optional.
196+-- | If the argument is supplied for the field the value is available in the arguments wrapped in
197+-- | `Just`. If the argument is not supplied or the supplied value is `null` the arguments field
198+-- | in the resolver is set to `Nothing`.
199+-- | The name argument tuple can then be used to be added to a field using the `?>` operator.
200+-- |
201+-- | *Example:*
202+-- | ```purescript
203+-- | objectType "Query"
204+-- | :> field "hello" Scalar.string
205+-- | ?> optionalArg Scalar.string (SProxy :: _ "name")
206+-- | !> (\{ name } parent -> case name of
207+-- | Just n -> pure $ "Hello " <> name
208+-- | Nothing -> pure "Hello stranger"
209+-- | )
210+-- | ```
211+optionalArg ::
212+ forall t a n.
213+ InputType t =>
214+ IsSymbol n =>
215+ t a ->
216+ SProxy n ->
217+ Tuple (SProxy n) (Argument (Maybe a))
218+optionalArg t name =
219+ Tuple (SProxy :: _ n) $
220+ Argument
221+ { description: Nothing
222+ , resolveValue
223+ , typeIntrospection: \_ -> introspect t
224+ }
225+ where
226+ -- I am not really happy with this function and it is a bit of a hack:
227+ -- We are basically proxying the input function and prevent it from failing if null is
228+ -- supplied as the variable value. It is easy to remove the Nothing case from the literal
229+ -- case but hard to remove the JSON null from the possible JSON values. I think this is the
230+ -- right place to implement this logic but I don't know how to remove this case completely
231+ -- from the input function.
232+ resolveValue Nothing execCtx = Right Nothing
233+ resolveValue (Just AST.NullValueNode) execCtx = Right Nothing
234+ resolveValue a@(Just (AST.VariableNode { name: AST.NameNode n})) execCtx = do
235+ variableValue <- note ("Unknown variable \"" <> n.value <> "\".") $
236+ lookup n.value execCtx.variables
237+ if Json.isNull variableValue then pure Nothing else Just <$> input t a execCtx
238+ resolveValue a execCtx = Just <$> input t a execCtx
239+
240+
241+inputObjectType :: String -> InputObjectType {}
242+inputObjectType name = InputObjectType \_ ->
243+ { name
244+ , description: Nothing
245+ , fieldIntrospection: []
246+ , input: \_ _ -> Right {}
247+ }
248+
249+
250+inputField :: forall t l a. IsSymbol l => InputType t => t a -> SProxy l -> InputField l a
251+inputField inputType label = InputField
252+ { name: label
253+ , required: true
254+ , introspection:
255+ IntrospectionTypes.InputValueIntrospection
256+ { name: reflectSymbol label
257+ , description: Nothing
258+ , defaultValue: Nothing
259+ , type: \_ -> IntrospectionTypes.NonNullTypeIntrospection
260+ { ofType: \_ -> introspect inputType
261+ }
262+ }
263+ , input: input inputType
264+ }
265+
266+
267+optionalInputField :: forall t l a.
268+ IsSymbol l =>
269+ InputType t =>
270+ t a ->
271+ SProxy l ->
272+ InputField l (Maybe a)
273+optionalInputField inputType label = InputField
274+ { name: label
275+ , required: false
276+ , introspection:
277+ IntrospectionTypes.InputValueIntrospection
278+ { name: reflectSymbol label
279+ , description: Nothing
280+ , defaultValue: Nothing
281+ , type: \_ -> introspect inputType
282+ }
283+ , input: \maybeValNode execContext ->
284+ case maybeValNode of
285+ Nothing -> Right Nothing
286+ Just AST.NullValueNode -> Right Nothing
287+ Just _ -> Just <$> input inputType maybeValNode execContext
288+ }
289+
290+
291+-- | The describe type class is used to allow adding descriptions to various parts of a GraphQL
292+-- | schema that can have a description. The `.>` operator can be used with the GraphQL object
293+-- | type DSL to easily add descriptions in various places.
294+-- |
295+-- | *Example:*
296+-- | ```purescript
297+-- | objectType "User"
298+-- | .> "A user of the product."
299+-- | :> field "id" Scalar.id
300+-- | .> "A unique identifier for this user."
301+-- | ```
302+class Describe a where
303+ describe :: a -> String -> a
304+
305+
306+infixl 8 describe as .>
307+
308+
309+instance describeObjectType :: Describe (ObjectType m a) where
310+ describe (ObjectType configFn) s =
311+ ObjectType $ \_ ->
312+ let
313+ config = configFn unit
314+ updateDescription (IntrospectionTypes.ObjectTypeIntrospection introspection) =
315+ IntrospectionTypes.ObjectTypeIntrospection $
316+ introspection { description = Just s }
317+ updateDescription other = other
318+ in
319+ config { introspection = updateDescription config.introspection }
320+
321+
322+instance describeField :: Describe (Field m a argsd argsp) where
323+ describe (Field config) s = Field (config { description = Just s })
324+
325+
326+instance describeArgument :: Describe (Argument a) where
327+ describe (Argument config) s = Argument (config { description = Just s })
328+
329+
330+instance describeArgumentTuple :: Describe (Tuple s (Argument a)) where
331+ describe (Tuple p (Argument config)) s = Tuple p (Argument (config { description = Just s }))
332+
333+
334+instance describeEnumType :: Describe (EnumType a) where
335+ describe (EnumType config) s = EnumType ( config { description = Just s })
336+
337+
338+instance describeEnumValue :: Describe (EnumValue a) where
339+ describe (EnumValue config) s = EnumValue ( config { description = Just s })
340+
341+
342+instance describeInputObjectType :: Describe (InputObjectType a) where
343+ describe (InputObjectType configFn) s =
344+ InputObjectType $ \_ -> (configFn unit) { description = Just s }
345+
346+
347+instance describeInputField :: Describe (InputField s a) where
348+ describe (InputField config) s =
349+ InputField $ config { introspection = i }
350+ where
351+ { introspection: (IntrospectionTypes.InputValueIntrospection introspection)} = config
352+ i = IntrospectionTypes.InputValueIntrospection $ introspection { description = Just s }
353+
354+
355+-- | The `withField` function is used to add fields to an object type.
356+-- |
357+-- | When added to an object type the information about the field's arguments are hidden in the
358+-- | closure by being converted to the `ExecutableField` type.
359+withField :: forall m a argsd argsp.
360+ ArgsDefToArgsParam argsd argsp =>
361+ MonadError Error m =>
362+ ObjectType m a ->
363+ Field m a argsd argsp ->
364+ ObjectType m a
365+withField (ObjectType objectConfigFn) fld@(
366+ Field { name, description, typeIntrospection, argumentIntrospections }
367+) =
368+ ObjectType updated
369+ where
370+ updated _ =
371+ let
372+ objectConfig = objectConfigFn unit
373+ updatedFields = insert name (makeExecutable fld) objectConfig.fields
374+
375+ fieldIntrospection = IntrospectionTypes.FieldIntrospection
376+ { name
377+ , description
378+ , args: argumentIntrospections
379+ , type: typeIntrospection
380+ , deprecationReason: Nothing
381+ }
382+
383+ updateIntrospection (IntrospectionTypes.ObjectTypeIntrospection introspection) =
384+ IntrospectionTypes.ObjectTypeIntrospection $
385+ introspection { fields = Array.snoc introspection.fields fieldIntrospection }
386+ updateIntrospection other = other
387+
388+ updatedIntrospection = updateIntrospection objectConfig.introspection
389+ in
390+ objectConfig { fields = updatedFields, introspection = updatedIntrospection }
391+
392+ makeExecutable :: Field m a argsd argsp -> ExecutableField m a
393+ makeExecutable (Field { args, serialize: serialize' }) = ExecutableField { execute: execute' }
394+ where
395+ execute' :: a -> AST.SelectionNode -> ExecutionContext -> m Result
396+ execute' val node@(AST.FieldNode { arguments: argumentNodes }) variables =
397+ case argsFromDefinition argumentNodes variables args of
398+ Right argumentValues -> serialize' node variables argumentValues val
399+ Left err -> pure (ResultError err)
400+ execute' _ _ _ = pure (ResultError "Unexpected non field node field execution...")
401+
402+
403+-- | Operator for the `withField` function: Add a field to an object type.
404+-- |
405+-- | *Example:*
406+-- | ```purescript
407+-- | objectType "Query"
408+-- | :> field "hello" Scalar.string
409+-- | ```
410+infixl 5 withField as :>
411+
412+
413+-- | A type class that contrains the relationship between defined arguments and the _argument_
414+-- | parameter of a resolver.
415+class ArgsDefToArgsParam (argsd :: # Type) (argsp :: # Type)
416+ | argsd -> argsp
417+ , argsp -> argsd where
418+ argsFromDefinition ::
419+ List AST.ArgumentNode ->
420+ ExecutionContext ->
421+ Record argsd ->
422+ Either String (Record argsp)
423+
424+
425+instance argsDefToArgsParamImpl ::
426+ ( RL.RowToList argsd largsd
427+ , RL.RowToList argsp largsp
428+ , ArgsFromRows largsd largsp argsd argsp
429+ , RL.ListToRow largsd argsd
430+ , RL.ListToRow largsp argsp ) => ArgsDefToArgsParam argsd argsp where
431+ argsFromDefinition = argsFromRows (RLProxy :: RLProxy largsd) (RLProxy :: RLProxy largsp)
432+
433+
434+-- | ArgsDefToArgsParam that works on row lists for matching implementations
435+class ArgsFromRows
436+ (largsd :: RL.RowList)
437+ (largsp :: RL.RowList)
438+ (argsd :: # Type)
439+ (argsp :: # Type)
440+ where
441+ argsFromRows ::
442+ RLProxy largsd ->
443+ RLProxy largsp ->
444+ List AST.ArgumentNode ->
445+ ExecutionContext ->
446+ Record argsd ->
447+ Either String (Record argsp)
448+
449+
450+instance argsFromRowsCons ::
451+ ( Row.Cons l (Argument a) targsd argsd
452+ , Row.Cons l a targsp argsp
453+ , Row.Lacks l targsd
454+ , Row.Lacks l targsp
455+ , ArgsFromRows ltargsd ltargsp targsd targsp
456+ , IsSymbol l ) =>
457+ ArgsFromRows
458+ (RL.Cons l (Argument a) ltargsd)
459+ (RL.Cons l a ltargsp)
460+ argsd
461+ argsp
462+ where
463+ argsFromRows argsdProxy argspProxy nodes variables argsd =
464+ let key = SProxy :: SProxy l
465+ Argument argConfig = Record.get key argsd
466+ tailArgsDef = Record.delete key argsd :: Record targsd
467+ nameEquals (AST.ArgumentNode { name: AST.NameNode { value } }) =
468+ value == reflectSymbol key
469+ argumentNode = find nameEquals nodes
470+ -- Extract the argument value if the node can be found. If not use null value
471+ -- TODO: Implement default values where we would try to use the default value first
472+ valueNode = map (\(AST.ArgumentNode { value }) -> value) argumentNode
473+ improveInputError err = "Error when reading argument '" <> reflectSymbol key <> "': " <> err
474+ resolvedValue = lmap improveInputError $ argConfig.resolveValue valueNode variables
475+ tail =
476+ argsFromRows
477+ (RLProxy :: RLProxy ltargsd)
478+ (RLProxy :: RLProxy ltargsp)
479+ nodes
480+ variables
481+ tailArgsDef
482+ in Record.insert key <$> resolvedValue <*> tail
483+
484+else instance argsFromRowsNil :: ArgsFromRows RL.Nil RL.Nil argsd argsp where
485+ argsFromRows _ _ _ _ _ = pure $ unsafeCoerce {}
486+
487+
488+-- | The `withArgument` function adds an argument to a field. The argument can then be used in
489+-- | resolvers that are added to the field. The old resolver of the field is still valid until it is
490+-- | overwritten by `withResolver.`
491+withArgument :: forall m a arg n argsdold argsdnew argspold argspnew.
492+ IsSymbol n =>
493+ Row.Cons n (Argument arg) argsdold argsdnew =>
494+ Row.Cons n arg argspold argspnew =>
495+ Row.Lacks n argsdold =>
496+ Row.Lacks n argspold =>
497+ ArgsDefToArgsParam argsdold argspold =>
498+ ArgsDefToArgsParam argsdnew argspnew =>
499+ Field m a argsdold argspold ->
500+ Tuple (SProxy n) (Argument arg) ->
501+ Field m a argsdnew argspnew
502+withArgument (Field fieldConfig) (Tuple proxy argument) =
503+ Field $ fieldConfig { args = args', serialize = serialize', argumentIntrospections = intros' }
504+ where
505+ args' :: Record argsdnew
506+ args' = Record.insert proxy argument fieldConfig.args
507+
508+ serialize' ::
509+ AST.SelectionNode ->
510+ ExecutionContext ->
511+ Record argspnew ->
512+ a ->
513+ m Result
514+ serialize' node variables argsnew val =
515+ fieldConfig.serialize node variables (Record.delete proxy argsnew) val
516+
517+ intros' :: Array IntrospectionTypes.InputValueIntrospection
518+ intros' = Array.snoc fieldConfig.argumentIntrospections
519+ (
520+ IntrospectionTypes.InputValueIntrospection
521+ { name: reflectSymbol proxy
522+ , description: _.description $ unwrap argument
523+ , type: _.typeIntrospection $ unwrap argument
524+ , defaultValue: Nothing
525+ }
526+ )
527+
528+
529+-- | Adds an argument to a field that can be used in the resolver.
530+-- |
531+-- | *Example:*
532+-- | ```purescript
533+-- | objectType "Query"
534+-- | :> field "hello" Scalar.string
535+-- | ?> arg (SProxy :: SProxy "name") Scalar.string
536+-- | !> (\{ name } parent -> "Hello " <> name <> "!")
537+-- | ```
538+infixl 7 withArgument as ?>
539+
540+
541+-- | The `withResolver` function adds a resolver to a field.
542+-- |
543+-- | _Note: When used multiple times on a field the resolvers are chained._
544+withResolver :: forall m a b argsd argsp.
545+ MonadError Error m =>
546+ Field m a argsd argsp ->
547+ (Record argsp -> b -> m a) ->
548+ Field m b argsd argsp
549+withResolver (Field fieldConfig) resolver =
550+ Field $ fieldConfig { serialize = serialize }
551+ where
552+ serialize :: AST.SelectionNode -> ExecutionContext -> Record argsp -> b -> m Result
553+ serialize node variables args = resolver args >=> fieldConfig.serialize node variables args
554+
555+
556+-- | Add a resolver to a field that receives the arguments in a record and the parent value.
557+-- |
558+-- | *Example:*
559+-- | ```purescript
560+-- | objectType "User"
561+-- | :> field "friends" Scalar.string
562+-- | !> (\_ user -> user >>= loadUserFriends)
563+-- | ```
564+infixl 6 withResolver as !>
565+
566+
567+-- | Add a resolver to a field that does not take the arguments. This is useful in the very common
568+-- | case that your field has no arguments.
569+withSimpleResolver :: forall m a b.
570+ MonadError Error m =>
571+ Field m a () () ->
572+ (b -> m a) ->
573+ Field m b () ()
574+withSimpleResolver fld resolver = withResolver fld (const resolver)
575+
576+
577+-- | Add a resolver to a field that receives only the parent value and ignores the arguments.
578+-- |
579+-- | *Example:*
580+-- | ```purescript
581+-- | objectType "User"
582+-- | :> field "friends" Scalar.string
583+-- | !!> (\user -> user >>= loadUserFriends)
584+-- | ```
585+infixl 6 withSimpleResolver as !!>
586+
587+
588+-- | Add a resolver that simply maps over the parent value. This is useful when you have a pure
589+-- | resolver that does not run effects.
590+withMappingResolver :: forall m a b.
591+ MonadError Error m =>
592+ Field m a () () ->
593+ (b -> a) ->
594+ Field m b () ()
595+withMappingResolver fld resolver = withSimpleResolver fld (resolver >>> pure)
596+
597+
598+-- | Add a pure resolver to a field that simply maps the parent value.
599+-- |
600+-- | *Example:*
601+-- | ```purescript
602+-- | objectType "User"
603+-- | :> field "id" Scalar.string
604+-- | !#> _.id
605+-- | ```
606+infixl 6 withMappingResolver as !#>
607+
608+
609+-- | Add a field to an input object type.
610+-- | Usually this functions operator alias
611+withInputField :: forall l a r1 r2.
612+ IsSymbol l
613+ => Row.Lacks l r1
614+ => Row.Cons l a r1 r2
615+ => InputObjectType { | r1 }
616+ -> InputField l a
617+ -> InputObjectType { | r2 }
618+withInputField (InputObjectType objConfig) (InputField inputConfig) = InputObjectType (\_ ->
619+ let
620+ config = (objConfig unit)
621+ fieldLabel = (SProxy :: SProxy l)
622+ fieldName = reflectSymbol fieldLabel
623+
624+ -- This function calls the old input function recursively and then also reads the new field
625+ -- from the provided object
626+ input :: Maybe AST.ValueNode -> ExecutionContext -> Either String (Record r2)
627+ input valueNode execCtx = do
628+ recValue <- config.input valueNode execCtx
629+ -- There are two possibilities to pass in input values: Inline or as a variable
630+ case valueNode of
631+ -- if it is a variable, we have to get the fields from JSON
632+ (Just (AST.VariableNode { name: AST.NameNode n })) ->
633+ case lookup n.value execCtx.variables of
634+ Just json -> do
635+ maybeValue <- lmap printJsonDecodeError $
636+ -- TODO: We should probably throw if the variable is not an object...
637+ Json.caseJsonObject (Right Nothing) (_ `Json.getFieldOptional'` fieldName) json
638+ let hackedVariables = insert "variable_hack" (fromMaybe Json.jsonNull maybeValue) empty
639+ let hackedCtx = { fragments: execCtx.fragments, variables: hackedVariables }
640+ fieldValue <-
641+ inputConfig.input
642+ (Just (AST.VariableNode { name: (AST.NameNode { value: "variable_hack" }) }))
643+ hackedCtx
644+ pure $ Record.insert fieldLabel fieldValue recValue
645+ Nothing -> Left ("Unknown variable \"" <> n.value <> "\".")
646+
647+ -- else we have to get the values from the node
648+ (Just (AST.ObjectValueNode { fields })) ->
649+ -- here we can simply search through all the fields and try to find our value
650+ case find (\(AST.ObjectFieldNode { name: AST.NameNode n }) -> n.value == fieldName) fields of
651+ Just (AST.ObjectFieldNode { value }) -> do
652+ fieldValue <- inputConfig.input (Just value) execCtx
653+ pure $ Record.insert fieldLabel fieldValue recValue
654+ Nothing ->
655+ if inputConfig.required
656+ then Left ("Field '" <> fieldName <> "' is required in object type.")
657+ else do
658+ fieldValue <- inputConfig.input Nothing execCtx
659+ pure $ Record.insert fieldLabel fieldValue recValue
660+
661+ _ -> Left "Must provide value for required input object value."
662+
663+ fieldIntrospection = Array.cons inputConfig.introspection config.fieldIntrospection
664+
665+ in config { input = input, fieldIntrospection = fieldIntrospection }
666+ )
667+
668+
669+-- | Add a field to an input object type.
670+-- |
671+-- | *Example:*
672+-- | ```purescript
673+-- | inputObjectType "User"
674+-- | :?> inputField "name" Scalar.string
675+-- | .> "The name of the user"
676+-- | ```
677+infixl 6 withInputField as :?>
678+
679+
680+-- | Take a bounded enum and infer an enum type from that value using it's show instance to
681+-- | represent the enum string. The show function cannot return values that are not valid GraphQL
682+-- | indentifier strings. For practical reasons this is not checked at compile time. Please refer to
683+-- | the tutorial for best practices around enum values.
684+enumType :: forall a. Bounded a => Enum a => Show a => String -> EnumType a
685+enumType name = EnumType { name, description: Nothing, values }
686+ where
687+ values = enumFromTo top bottom <#> \value ->
688+ EnumValue { name: show value, description: Nothing, value, isValue: eq value }
689+
690+
691+class UnionDefinition (defRow :: # Type) (varRow :: # Type) (ctx :: Type -> Type)
692+ | defRow -> varRow
693+ , varRow -> defRow where
694+ union ::
695+ String ->
696+ Record defRow ->
697+ UnionType ctx (Variant.Variant varRow)
698+
699+
700+-- This instance is only to get the row list type from the record
701+instance unionDefinitionInstance ::
702+ ( RL.RowToList defRow defRowList
703+ , RL.RowToList varRow varRowList
704+ , UnionIntrospection defRowList defRow
705+ , UnionResolver defRowList varRowList defRow varRow ctx
706+ , MonadError Error ctx
707+ , RL.ListToRow defRowList defRow
708+ , RL.ListToRow varRowList varRow ) => UnionDefinition defRow varRow ctx where
709+ union name defRecord =
710+ let
711+ typeIntrospections = unionIntrospection (RLProxy :: RLProxy defRowList) defRecord
712+ output selectionSetNode execCtx ctxValue = ctxValue >>=
713+ unionResolver
714+ (RLProxy :: RLProxy defRowList)
715+ (RLProxy :: RLProxy varRowList)
716+ defRecord
717+ selectionSetNode
718+ execCtx
719+ in
720+ UnionType $ \_ -> { name, description: Nothing, typeIntrospections, output }
721+
722+
723+class UnionIntrospection
724+ (defRowList :: RL.RowList)
725+ (defRow :: # Type)
726+ where
727+ unionIntrospection ::
728+ RLProxy defRowList ->
729+ Record defRow ->
730+ Array IntrospectionTypes.TypeIntrospection
731+
732+
733+instance unionIntrospectionCons ::
734+ ( Row.Cons l (ObjectType ctx a) defRowTail defRow
735+ , Row.Lacks l defRowTail
736+ , UnionIntrospection defRowListTail defRowTail
737+ , IsSymbol l
738+ ) =>
739+ UnionIntrospection (RL.Cons l (ObjectType ctx a) defRowListTail) defRow
740+ where
741+ unionIntrospection defRLProxy defRecord =
742+ let
743+ ObjectType configFn = Record.get (SProxy :: SProxy l) defRecord
744+ config = configFn unit
745+ in
746+ -- Recursively call function
747+ Array.cons config.introspection $
748+ unionIntrospection
749+ (RLProxy :: RLProxy defRowListTail)
750+ -- We will just read from this value
751+ (unsafeCoerce defRecord :: Record defRowTail)
752+
753+else instance unionIntrospectionNil :: UnionIntrospection RL.Nil defRow where
754+ unionIntrospection _ _ = []
755+
756+
757+class UnionResolver
758+ (defRowList :: RL.RowList)
759+ (varRowList :: RL.RowList)
760+ (defRow :: # Type)
761+ (varRow :: # Type)
762+ (ctx :: Type -> Type)
763+ where
764+ unionResolver ::
765+ RLProxy defRowList ->
766+ RLProxy varRowList ->
767+ Record defRow ->
768+ Maybe AST.SelectionSetNode ->
769+ ExecutionContext ->
770+ (Variant.Variant varRow) ->
771+ ctx Result
772+
773+
774+instance unionResolverCons ::
775+ ( Row.Cons l (ObjectType ctx a) defRowTail defRow
776+ , Row.Cons l a varRowTail varRow
777+ , Row.Lacks l defRowTail
778+ , Row.Lacks l varRowTail
779+ , UnionResolver defRowListTail varRowListTail defRowTail varRowTail ctx
780+ , MonadError Error ctx
781+ , IsSymbol l
782+ ) =>
783+ UnionResolver
784+ (RL.Cons l (ObjectType ctx a) defRowListTail)
785+ (RL.Cons l a varRowListTail)
786+ defRow
787+ varRow
788+ ctx
789+ where
790+ unionResolver ::
791+ RLProxy (RL.Cons l (ObjectType ctx a) defRowListTail) ->
792+ RLProxy (RL.Cons l a varRowListTail) ->
793+ Record defRow ->
794+ Maybe AST.SelectionSetNode ->
795+ ExecutionContext ->
796+ (Variant.Variant varRow) ->
797+ ctx Result
798+ unionResolver defRLProxy varRLProxy defRecord Nothing execCtx =
799+ const $ throwError $ error "Missing selection set for union type"
800+ unionResolver defRLProxy varRLProxy defRecord (Just selection) execCtx =
801+ let
802+ objectType@(ObjectType config) = Record.get (SProxy :: SProxy l) defRecord
803+ typename = (config unit).name
804+ filteredSelection = filterSelectionSet typename selection execCtx
805+ recResolver ::
806+ (Variant.Variant varRowTail) ->
807+ ctx Result
808+ recResolver =
809+ unionResolver
810+ (RLProxy :: RLProxy defRowListTail)
811+ (RLProxy :: RLProxy varRowListTail)
812+ (unsafeCoerce defRecord :: Record defRowTail)
813+ (Just selection)
814+ execCtx
815+ in
816+ recResolver
817+ # Variant.on (SProxy :: SProxy l)
818+ (pure >>> output objectType (Just filteredSelection) execCtx)
819+
820+else instance unionResolverNil :: Functor ctx => UnionResolver RL.Nil RL.Nil defRow varRow ctx where
821+ unionResolver _ _ _ _ _ = unsafeCoerce >>> Variant.case_
822+
src/GraphQL/Execution.purs+4−2
@@ -8,16 +8,18 @@ import Data.Argonaut.Encode ((:=), (~>))
88import Data.List (List(..), find)
99import Data.Maybe (Maybe(..), fromMaybe, maybe)
1010import Effect.Exception (Error, error)
11+import GraphQL.DSL (field, (!#>), (:>))
1112import GraphQL.Execution.Result (serializeResult)
1213import GraphQL.Language.AST (DefinitionNode(..), DocumentNode(..), NameNode(..), OperationTypeNode(..))
13−import GraphQL.Type (ObjectType, Schema(..), ExecutionContext, field, output, (!#>), (:>), introspect)
14−import GraphQL.Type.Introspection (schemaType)
14+import GraphQL.Type (ObjectType, Schema(..), ExecutionContext, output, introspect)
15+import GraphQL.Builtin.Introspection (schemaType)
1516import GraphQL.Type.Introspection.Datatypes (SchemaIntrospection(..))
1617
1718
1819simpleError :: forall m. Applicative m => String -> m Json
1920simpleError e = pure $ "error" := e ~> jsonEmptyObject
2021
22+
2123-- | This function is mostly used internally. If you just want to execute a GraphQL query you should
2224-- | probably use `graphql` from the `GraphQL` module.
2325-- |
src/GraphQL/Execution/FlattenAst.pursnew file+50
@@ -0,0 +1,50 @@
1+-- | This module is a little bit of a hack and it might be replaced in the future by a real
2+-- | transformation from an AST to a normalized AST. Here we will keep the data structures exported
3+-- | from the AST module and replace fragments by inlining them with their selection set.
4+module GraphQL.Execution.FlattenAst (flattenAst) where
5+
6+import Prelude
7+
8+import Data.Foldable (find, foldl)
9+import Data.List as List
10+import Data.Map as Map
11+import Data.Maybe (Maybe(..), fromMaybe)
12+import GraphQL.Language.AST as AST
13+import GraphQL.Language.AST.Util (nameFromNameNode, selectionsFromSelectionSet)
14+
15+
16+flattenAst :: AST.DocumentNode -> AST.DocumentNode
17+flattenAst (AST.DocumentNode doc) = AST.DocumentNode $ doc { definitions = defs }
18+ where
19+ fragments = doc.definitions `flip List.mapMaybe` case _ of
20+ (AST.FragmentDefinitionNode config) -> Just config
21+ _ -> Nothing
22+
23+ fragmentsMap :: Map.Map String AST.SelectionSetNode
24+ fragmentsMap =
25+ foldl (\m c -> Map.insert (nameFromNameNode c.name) c.selectionSet m) Map.empty fragments
26+
27+ findFragment fragmentName = find (_.name >>> (_ == fragmentName)) fragments
28+
29+ replaceFragment :: AST.SelectionNode -> List.List AST.SelectionNode
30+ replaceFragment f@(AST.FieldNode _) = List.singleton f
31+ replaceFragment f@(AST.FragmentSpreadNode { name }) =
32+ fromMaybe (List.singleton f) $
33+ selectionsFromSelectionSet <$>
34+ flattenSelectionSet <$>
35+ _.selectionSet <$>
36+ findFragment name
37+ replaceFragment (AST.InlineFragmentNode { selectionSet }) =
38+ selectionsFromSelectionSet $ flattenSelectionSet selectionSet
39+
40+ flattenSelectionSet :: AST.SelectionSetNode -> AST.SelectionSetNode
41+ flattenSelectionSet (AST.SelectionSetNode set) = AST.SelectionSetNode { selections }
42+ where
43+ selections = set.selections >>= replaceFragment
44+
45+ defs = doc.definitions `flip List.mapMaybe` case _ of
46+ (AST.OperationDefinitionNode def) ->
47+ Just $ AST.OperationDefinitionNode $
48+ def { selectionSet = flattenSelectionSet def.selectionSet }
49+
50+ _ -> Nothing
src/GraphQL/Execution/Normalize.pursnew file+42
@@ -0,0 +1,42 @@
1+-- | This module is used to normalize the query AST into an Executable Syntax Tree
2+module GraphQL.Execution.Normalize where
3+
4+import Prelude
5+
6+import Data.Either (Either(..))
7+import Data.List (List(..), filter, find, mapMaybe)
8+import Data.Maybe (Maybe(..))
9+import GraphQL.Language.AST as AST
10+
11+newtype NormalizedOperationNode =
12+ NormalizedOperationNode { }
13+
14+newtype NormalizedSelectionSetNode =
15+ NormalizedSelectionSetNode { selections :: List NormalizedSelectionNode }
16+
17+data NormalizedSelectionNode
18+ = NormalizedFieldSelectionNode {}
19+
20+normaliseAst :: AST.DocumentNode -> Maybe String -> Either String NormalizedOperationNode
21+normaliseAst (AST.DocumentNode { definitions }) operationName = Left "TODO"
22+ where
23+ -- In GraphQL the document can potentially contain multiple operations.
24+ -- In this case the request needs to supply a parameter called "operationName" the defines
25+ -- which of the operations should be executed.
26+ -- See: https://graphql.github.io/graphql-spec/June2018/#sec-Executing-Requests
27+ operation = case filter selectedOperationFilter definitions of
28+ Cons op Nil -> pure op
29+ Nil -> Left "No operation provided for execution."
30+ _ -> Left "Multiple operations provided for execution."
31+
32+ selectedOperationFilter (AST.OperationDefinitionNode config) = case operationName of
33+ Nothing -> true
34+ op -> op == map (\(AST.NameNode { value }) -> value) config.name
35+ selectedOperationFilter _ = false
36+
37+ -- Collect all operations that are fragments
38+ fragments = definitions `flip mapMaybe` case _ of
39+ (AST.FragmentDefinitionNode config) -> Just config
40+ _ -> Nothing
41+
42+ findFragment fragmentName = find (_.name >>> (_ == fragmentName)) fragments
src/GraphQL/Language/Printer.pursnew file+80
@@ -0,0 +1,80 @@
1+module GraphQL.Language.Printer where
2+
3+import Prelude
4+
5+import Data.Foldable (intercalate)
6+import Data.String (Pattern(..), split)
7+import GraphQL.Language.AST as AST
8+
9+data Doc
10+ = Nil
11+ | Text String Doc
12+ | Line Int Doc
13+
14+instance semigroupDoc :: Semigroup Doc where
15+ append Nil d = d
16+ append d Nil = d
17+ append (Text s1 Nil) (Text s2 ds) = Text (s1 <> s2) ds
18+ append (Text s Nil) d = Text s d
19+ append (Text s ds) d = Text s (append ds d)
20+ append (Line i Nil) d = Line i d
21+ append (Line i ds) d = Line i (append ds d)
22+
23+instance monoidDoc :: Monoid Doc where
24+ mempty = Nil
25+
26+text :: String -> Doc
27+text str = Text str Nil
28+
29+line :: Doc
30+line = Line 0 Nil
31+
32+nest :: Int -> Doc -> Doc
33+nest indent Nil = Nil
34+nest indent (Text s ds) = Text s (nest indent ds)
35+nest indent (Line i ds) = Line (i + indent) (nest indent ds)
36+
37+layout :: Doc -> String
38+layout Nil = ""
39+layout (Text s ds) = s <> (layout ds)
40+layout (Line i ds) = "\n" <> repeat " " i <> layout ds
41+
42+repeat :: String -> Int -> String
43+repeat str times = repeatRec str times ""
44+ where
45+ repeatRec _ 0 acc = acc
46+ repeatRec s i acc = repeatRec s (i - 1) (s <> acc)
47+
48+printNameNode :: AST.NameNode -> Doc
49+printNameNode (AST.NameNode { value }) = text value
50+
51+printSimpleNamedTypeNode :: AST.SimpleNamedTypeNode -> Doc
52+printSimpleNamedTypeNode (AST.SimpleNamedTypeNode { name }) = printNameNode name
53+
54+printTypeNode :: AST.TypeNode -> Doc
55+printTypeNode (AST.NamedTypeNode { "type": t }) = printSimpleNamedTypeNode t
56+printTypeNode (AST.NonNullTypeNode { "type": t }) = printTypeNode t <> text "!"
57+printTypeNode (AST.ListTypeNode { "type": t }) = text "[" <> printTypeNode t <> text "]"
58+
59+printObjectFieldNode :: AST.ObjectFieldNode -> Doc
60+printObjectFieldNode (AST.ObjectFieldNode { name, value }) =
61+ printNameNode name <> text ": " <> printValueNode value
62+
63+printValueNode :: AST.ValueNode -> Doc
64+printValueNode (AST.VariableNode { name }) = text "$" <> printNameNode name
65+printValueNode (AST.IntValueNode { value }) = text value
66+printValueNode (AST.FloatValueNode { value }) = text value
67+printValueNode (AST.StringValueNode { value, block }) =
68+ if block then printBlockString value else text ("\"" <> value <> "\"")
69+printValueNode (AST.BooleanValueNode { value }) = if value then text "true" else text "false"
70+printValueNode AST.NullValueNode = text "null"
71+printValueNode (AST.EnumValueNode { name }) = printNameNode name
72+printValueNode (AST.ListValueNode { values }) =
73+ text "[" <> intercalate (text ", ") (map printValueNode values) <> text "]"
74+printValueNode (AST.ObjectValueNode { fields }) =
75+ text "{" <> intercalate (text ", ") (map printObjectFieldNode fields) <> text "}"
76+
77+printBlockString :: String -> Doc
78+printBlockString str =
79+ let lines = map text (split (Pattern "\n") str)
80+ in text "\"\"\"" <> line <> intercalate line lines <> line <> text "\"\"\""
src/GraphQL/Type.purs+17−1023
@@ -1,1023 +1,17 @@
1−module GraphQL.Type where
2−
3−import Prelude
4−
5−import Control.Lazy (class Lazy)
6−import Control.Monad.Error.Class (class MonadError, catchError, throwError)
7−import Data.Argonaut as Json
8−import Data.Array as Array
9−import Data.Bifunctor (lmap)
10−import Data.Either (Either(..), note)
11−import Data.Enum (class Enum, enumFromTo)
12−import Data.List (List, fromFoldable, singleton, filter)
13−import Data.Map (Map, empty, insert, lookup)
14−import Data.Maybe (Maybe(..), fromMaybe)
15−import Data.Newtype (class Newtype, unwrap)
16−import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol)
17−import Data.Traversable (class Traversable, find, traverse)
18−import Data.Tuple (Tuple(..))
19−import Data.Variant as Variant
20−import Effect.Exception (Error, error, message)
21−import GraphQL.Execution.Result (Result(..))
22−import GraphQL.Language.AST as AST
23−import GraphQL.Language.AST.Util (nameFromNameNode)
24−import GraphQL.Type.Introspection.Datatypes as IntrospectionTypes
25−import Prim.Row (class Lacks, class Cons)
26−import Record as Record
27−import Type.Data.RowList (RLProxy(..))
28−import Type.Row as Row
29−import Type.RowList as RL
30−import Unsafe.Coerce (unsafeCoerce)
31−
32−-- | The schema contains the central entry points for GraphQL queries.
33−newtype Schema m a = Schema { query :: ObjectType m a, mutation :: Maybe (ObjectType m a) }
34−
35−type ExecutionContext =
36− { variables :: Map String Json.Json
37− , fragments :: Map String AST.DefinitionNode
38− }
39−
40−-- | The type class for all GraphQL types
41−class GraphQLType t where
42− introspect :: forall a. t a -> IntrospectionTypes.TypeIntrospection
43−
44−-- | The input type class is used for all GraphQL types that can be used as input types.
45−class GraphQLType t <= InputType t where
46− input :: forall a. t a -> Maybe AST.ValueNode -> ExecutionContext -> Either String a
47−
48−-- | The output type class is used for all GraphQL types that can be used as output types.
49−-- | It has instances for GraphQL types that can resolve a value within a certain monad error.
50−class (MonadError Error m, GraphQLType t) <= OutputType m t where
51− output :: forall a. t a -> Maybe AST.SelectionSetNode -> ExecutionContext -> m a -> m Result
52−
53−-- | Scalar types are the leaf nodes of a GraphQL schema. Scalar types can be serialised into JSON
54−-- | or obtained from a JSON when read from the variables provided by a query. Furthermore values
55−-- | can be read from literals in a GraphQL document. To create a scalar type a name and optionally
56−-- | a description need to be provided.
57−-- | The functions `parseLiteral`, `parseValue` and `serialize` are used to convert between the
58−-- | GraphQL transport formats and PureScript representations.
59−-- |
60−-- | The GraphQL PureScript implementation comes with the five default scalars defined in the
61−-- | specification.
62−newtype ScalarType a =
63− ScalarType
64− { name :: String
65− , description :: Maybe String
66− , parseLiteral :: AST.ValueNode -> Either String a
67− , parseValue :: Json.Json -> Either String a
68− , serialize :: a -> Json.Json
69− }
70−
71−instance graphqlTypeScalarType :: GraphQLType ScalarType where
72− introspect (ScalarType { name, description }) =
73− IntrospectionTypes.ScalarTypeIntrospection { name, description }
74−
75−instance inputTypeScalarType :: InputType ScalarType where
76− input (ScalarType config) (Just node) execCtx = case node of
77− (AST.VariableNode { name: AST.NameNode { value }}) -> do
78− json <- note ("Required variable `" <> value <> "` was not provided.") $
79− lookup value execCtx.variables
80− config.parseValue json
81− _ -> config.parseLiteral node
82− input _ Nothing _ = Left "Must provide value for required scalar."
83−
84−instance outputTypeScalarType :: (MonadError Error m) => OutputType m ScalarType where
85− output (ScalarType s) Nothing _ val = ResultLeaf <$> map s.serialize val
86− output _ _ _ _ = pure $ ResultError "Invalid subselection on scalar type!"
87−
88−instance showScalarType :: Show (ScalarType a) where
89− show (ScalarType { name }) = "scalar " <> name
90−
91−newtype ObjectType m a =
92− ObjectType (Unit ->
93− { name :: String
94− , fields :: Map String (ExecutableField m a)
95− , introspection :: IntrospectionTypes.TypeIntrospection
96− }
97− )
98−
99−instance graphqlTypeObjectType :: GraphQLType (ObjectType m) where
100− introspect (ObjectType configFn) = (configFn unit).introspection
101−
102−instance outputTypeObjectType :: (MonadError Error m) => OutputType m (ObjectType m) where
103− output (ObjectType fno) (Just (AST.SelectionSetNode { selections })) execCtx mValue = do
104− value <- mValue
105− let serializeField node@(AST.FieldNode fld) =
106− let (AST.NameNode { value: name }) = fld.name
107− (AST.NameNode { value: alias }) = fromMaybe fld.name fld.alias
108− o = fno unit
109− in case lookup name o.fields of
110− Just (ExecutableField { execute }) ->
111− Tuple alias <$>
112− (flip catchError (message >>> ResultError >>> pure) $
113− execute value node execCtx)
114− Nothing ->
115− if
116− name == "__typename"
117− then
118− pure $ Tuple alias $ ResultLeaf $ Json.fromString o.name
119− else
120− pure $ Tuple alias $
121− ResultError ("Unknown field `" <> name <> "` on type `" <> o.name <> "`.")
122− -- TODO: This branch needs fixing. It is for fragment spreads and so on. Maybe normalise
123− -- the query first and completely eliminate the branch...
124− serializeField _ = pure $ Tuple "unknown" $ ResultError "Unexpected fragment spread!"
125− ResultObject <$> traverse serializeField (selections >>= collectField)
126− where
127− -- Flatten the selection node structure by spreading all fragments into the list
128− collectField :: AST.SelectionNode -> List AST.SelectionNode
129− collectField n@(AST.FieldNode _) = singleton n
130− collectField (AST.InlineFragmentNode {
131− selectionSet: (AST.SelectionSetNode { selections: s })
132− }) = collectField =<< s
133− collectField (AST.FragmentSpreadNode { name: AST.NameNode n }) =
134− case lookup n.value execCtx.fragments of
135− Just (AST.FragmentDefinitionNode {
136− selectionSet: AST.SelectionSetNode { selections: s }
137− }) -> collectField =<< s
138− _ -> mempty
139−
140−
141− output _ _ _ _ = pure $ ResultError "Missing subselection on object type."
142−
143−
144−instance showObjectType :: Show (ObjectType m a) where
145− show (ObjectType config) = "type " <> (config unit).name
146−
147−derive instance newtypeObjectType :: Newtype (ObjectType m a) _
148−
149−instance lazyObjectType :: Lazy (ObjectType m a) where
150− defer fn = ObjectType $ \_ -> unwrap (fn unit) unit
151−
152−-- | The executable field loses the information about it's arguments types. This is needed to add it
153−-- | to the map of arguments of the object type. The execute function will extract the arguments of
154−-- | the field from the AST.
155−newtype ExecutableField m a =
156− ExecutableField { execute :: a -> AST.SelectionNode -> ExecutionContext -> m Result }
157−
158−-- | Object types can have fields. Fields are constructed using the `field` function from this
159−-- | module and added to object types using the `:>` operator.
160−newtype Field m a argsd argsp =
161− Field
162− { name :: String
163− , description :: Maybe String
164− , typeIntrospection :: Unit -> IntrospectionTypes.TypeIntrospection
165− , argumentIntrospections :: Array IntrospectionTypes.InputValueIntrospection
166− , args :: Record argsd
167− , serialize :: AST.SelectionNode -> ExecutionContext -> Record argsp -> a -> m Result
168− }
169−
170−-- | Fields can have multiple arguments. Arguments are constructed using the `arg` function from
171−-- | this module and the `?>` operator.
172−newtype Argument a =
173− Argument
174− { description :: Maybe String
175− , typeIntrospection :: Unit -> IntrospectionTypes.TypeIntrospection
176− , resolveValue :: Maybe AST.ValueNode -> ExecutionContext -> Either String a
177− }
178−
179−derive instance newtypeArgument :: Newtype (Argument a) _
180−
181−newtype EnumType a =
182− EnumType
183− { name :: String
184− , description :: Maybe String
185− , values :: Array (EnumValue a)
186− }
187−
188−newtype EnumValue a =
189− EnumValue
190− { name :: String
191− , description :: Maybe String
192− , value :: a
193− , isValue :: a -> Boolean }
194−
195−instance graphqlTypeEnumType :: GraphQLType EnumType where
196− introspect (EnumType { name, description, values }) =
197− IntrospectionTypes.EnumTypeIntrospection
198− { name
199− , description
200− , enumValues: values <#> \(EnumValue val) ->
201− IntrospectionTypes.EnumValueIntrospection
202− { name: val.name
203− , description: val.description
204− , deprecationReason: Nothing
205− }
206− }
207−
208−instance inputTypeEnumType :: InputType EnumType where
209− input (EnumType config) (Just node) execContext =
210− let lookupName name =
211− note ("Unknown enum value `" <> name <> "` for type `" <> config.name <> "`.") $
212− find (\(EnumValue val) -> val.name == name) config.values
213− in case node of
214− AST.VariableNode { name: AST.NameNode { value: name } } -> do
215− json <- note ("Required variable `" <> name <> "` was not provided.") $
216− lookup name execContext.variables
217− EnumValue val <- Json.caseJsonString (Left "Enum values must be strings.") lookupName json
218− pure val.value
219−
220− AST.EnumValueNode { name: AST.NameNode { value: name } } -> do
221− EnumValue val <- lookupName name
222− pure val.value
223−
224− _ -> Left "Unexpected non-enum value node for enum value."
225−
226− input _ _ _ = Left "Missing value for required argument."
227−
228−instance outputTypeEnumType :: MonadError Error m => OutputType m EnumType where
229− output :: forall a. EnumType a -> Maybe AST.SelectionSetNode -> ExecutionContext -> m a -> m Result
230− output (EnumType config) Nothing _ mValue = do
231− value <- mValue
232− case find (\(EnumValue { isValue }) -> isValue value) config.values of
233− Nothing ->
234− throwError $ error "Could not select enum value representation from provided value."
235−
236− Just (EnumValue val) ->
237− pure $ ResultLeaf $ Json.fromString val.name
238−
239− output _ _ _ _ = pure $ ResultError "Invalid subselection on enum type!"
240−
241−newtype UnionType m a = UnionType (Unit ->
242− { name :: String
243− , description :: Maybe String
244− , typeIntrospections :: Array IntrospectionTypes.TypeIntrospection
245− , output :: Maybe AST.SelectionSetNode -> ExecutionContext -> m a -> m Result
246− }
247−)
248−
249−instance graphqlTypeUnionType :: GraphQLType (UnionType m) where
250− introspect (UnionType config) = IntrospectionTypes.UnionTypeIntrospection
251− { name: (config unit).name
252− , description: (config unit).description
253− , possibleTypes: \_ -> (config unit).typeIntrospections
254− }
255−
256−-- Takes a string and only returns part of the selection set that matches the return type
257−-- TODO: Throw under certain cases, e.g. if there is a field selected in the root that is not
258−-- `__typename`
259−filterSelectionSet :: String -> AST.SelectionSetNode -> ExecutionContext -> AST.SelectionSetNode
260−filterSelectionSet typeName (AST.SelectionSetNode { selections }) ctx =
261− AST.SelectionSetNode { selections: filter filterFn selections }
262− where
263− -- Only the __typename field is allowed on the root selection
264− filterFn (AST.FieldNode { name: AST.NameNode { value }}) = value == "__typename"
265−
266− -- If we find a fragment we only include it into the selection if it matches the type
267− filterFn (AST.FragmentSpreadNode { name: AST.NameNode { value }}) =
268− case lookup value ctx.fragments of
269− Just (AST.FragmentDefinitionNode { typeCondition: AST.SimpleNamedTypeNode { name }}) ->
270− nameFromNameNode name == typeName
271− _ -> false
272−
273− -- Same thing for inline fragment only here we don't have to lookup the fragment
274− filterFn (AST.InlineFragmentNode { typeCondition: AST.SimpleNamedTypeNode { name } }) =
275− nameFromNameNode name == typeName
276−
277−instance outputTypeUnionType :: (MonadError Error m) => OutputType m (UnionType m) where
278− output :: forall a. UnionType m a -> Maybe AST.SelectionSetNode -> ExecutionContext -> m a -> m Result
279− output (UnionType config) = (config unit).output
280−
281−union :: forall ctx def var. UnionDefinition def var ctx => String -> Record def -> UnionType ctx (Variant.Variant var)
282−union = createUnionFromDefinition
283−
284−class UnionDefinition (defRow :: # Type) (varRow :: # Type) (ctx :: Type -> Type)
285− | defRow -> varRow
286− , varRow -> defRow where
287− createUnionFromDefinition ::
288− String ->
289− Record defRow ->
290− UnionType ctx (Variant.Variant varRow)
291−
292−-- This instance is only to get the row list type from the record
293−instance unionDefinitionInstance ::
294− ( RL.RowToList defRow defRowList
295− , RL.RowToList varRow varRowList
296− , UnionIntrospection defRowList defRow
297− , UnionResolver defRowList varRowList defRow varRow ctx
298− , MonadError Error ctx
299− , RL.ListToRow defRowList defRow
300− , RL.ListToRow varRowList varRow ) => UnionDefinition defRow varRow ctx where
301− createUnionFromDefinition name defRecord =
302− let
303− typeIntrospections = unionIntrospection (RLProxy :: RLProxy defRowList) defRecord
304− output selectionSetNode execCtx ctxValue = ctxValue >>=
305− unionResolver
306− (RLProxy :: RLProxy defRowList)
307− (RLProxy :: RLProxy varRowList)
308− defRecord
309− selectionSetNode
310− execCtx
311− in
312− UnionType $ \_ -> { name, description: Nothing, typeIntrospections, output }
313−
314−class UnionIntrospection
315− (defRowList :: RL.RowList)
316− (defRow :: # Type)
317− where
318− unionIntrospection ::
319− RLProxy defRowList ->
320− Record defRow ->
321− Array IntrospectionTypes.TypeIntrospection
322−
323−instance unionIntrospectionCons ::
324− ( Row.Cons l (ObjectType ctx a) defRowTail defRow
325− , Row.Lacks l defRowTail
326− , UnionIntrospection defRowListTail defRowTail
327− , IsSymbol l
328− ) =>
329− UnionIntrospection (RL.Cons l (ObjectType ctx a) defRowListTail) defRow
330− where
331− unionIntrospection defRLProxy defRecord =
332− let
333− ObjectType configFn = Record.get (SProxy :: SProxy l) defRecord
334− config = configFn unit
335− in
336− -- Recursively call function
337− Array.cons config.introspection $
338− unionIntrospection
339− (RLProxy :: RLProxy defRowListTail)
340− -- We will just read from this value
341− (unsafeCoerce defRecord :: Record defRowTail)
342−
343−else instance unionIntrospectionNil :: UnionIntrospection RL.Nil defRow where
344− unionIntrospection _ _ = []
345−
346−class UnionResolver
347− (defRowList :: RL.RowList)
348− (varRowList :: RL.RowList)
349− (defRow :: # Type)
350− (varRow :: # Type)
351− (ctx :: Type -> Type)
352− where
353− unionResolver ::
354− RLProxy defRowList ->
355− RLProxy varRowList ->
356− Record defRow ->
357− Maybe AST.SelectionSetNode ->
358− ExecutionContext ->
359− (Variant.Variant varRow) ->
360− ctx Result
361−
362−instance unionResolverCons ::
363− ( Row.Cons l (ObjectType ctx a) defRowTail defRow
364− , Row.Cons l a varRowTail varRow
365− , Row.Lacks l defRowTail
366− , Row.Lacks l varRowTail
367− , UnionResolver defRowListTail varRowListTail defRowTail varRowTail ctx
368− , MonadError Error ctx
369− , IsSymbol l
370− ) =>
371− UnionResolver
372− (RL.Cons l (ObjectType ctx a) defRowListTail)
373− (RL.Cons l a varRowListTail)
374− defRow
375− varRow
376− ctx
377− where
378− unionResolver ::
379− RLProxy (RL.Cons l (ObjectType ctx a) defRowListTail) ->
380− RLProxy (RL.Cons l a varRowListTail) ->
381− Record defRow ->
382− Maybe AST.SelectionSetNode ->
383− ExecutionContext ->
384− (Variant.Variant varRow) ->
385− ctx Result
386− unionResolver defRLProxy varRLProxy defRecord Nothing execCtx =
387− const $ throwError $ error "Missing selection set for union type"
388− unionResolver defRLProxy varRLProxy defRecord (Just selection) execCtx =
389− let
390− objectType@(ObjectType config) = Record.get (SProxy :: SProxy l) defRecord
391− typename = (config unit).name
392− filteredSelection = filterSelectionSet typename selection execCtx
393− recResolver ::
394− (Variant.Variant varRowTail) ->
395− ctx Result
396− recResolver =
397− unionResolver
398− (RLProxy :: RLProxy defRowListTail)
399− (RLProxy :: RLProxy varRowListTail)
400− (unsafeCoerce defRecord :: Record defRowTail)
401− (Just selection)
402− execCtx
403− in
404− recResolver
405− # Variant.on (SProxy :: SProxy l)
406− (pure >>> output objectType (Just filteredSelection) execCtx)
407−
408−else instance unionResolverNil :: Functor ctx => UnionResolver RL.Nil RL.Nil defRow varRow ctx where
409− unionResolver _ _ _ _ _ = unsafeCoerce >>> Variant.case_
410−
411−newtype InputObjectType a = InputObjectType (Unit ->
412− { name :: String
413− , description :: Maybe String
414− , fieldIntrospection :: Array IntrospectionTypes.InputValueIntrospection
415− , input :: Maybe AST.ValueNode -> ExecutionContext -> Either String a
416− }
417−)
418−
419−instance graphqlTypeInputObjectType :: GraphQLType InputObjectType where
420− introspect (InputObjectType config) =
421− IntrospectionTypes.InputObjectTypeIntrospection
422− { name: (config unit).name
423− , description: (config unit).description
424− , inputFields: (config unit).fieldIntrospection
425− }
426−
427−instance inputTypeInputObjectType :: InputType InputObjectType where
428− input (InputObjectType config) = (config unit).input
429−
430−derive instance newtypeInputObjectType :: Newtype (InputObjectType a) _
431−
432−instance lazyInputObjectType :: Lazy (InputObjectType a) where
433− defer fn = InputObjectType $ \_ -> unwrap (fn unit) unit
434−
435−newtype InputField l a = InputField
436− { name :: SProxy l
437− , introspection :: IntrospectionTypes.InputValueIntrospection
438− , input :: Maybe AST.ValueNode -> ExecutionContext -> Either String a
439− }
440−
441−-- * DSL Creator functions
442−
443−-- | Creates an empty object type with the given name. Fields can be added to the object type using
444−-- | the `:>` operator from this module and the `field` functions. Object types should have at least
445−-- | one field to confirm to the GraphQL specification.
446−-- |
447−-- | *Example:*
448−-- | ```purescript
449−-- | queryType :: ObjectType Unit
450−-- | queryType = objectType "Query"
451−-- | ```
452−objectType :: forall m a. String -> ObjectType m a
453−objectType name =
454− let
455− introspection = IntrospectionTypes.ObjectTypeIntrospection
456− { name, description: Nothing, fields: [] }
457− in
458− ObjectType (\_ -> { name, fields: empty, introspection })
459−
460−-- | Create a new field for an object type. This function is typically used together with the
461−-- | `:>` operator that automatically converts the field into an executable field.
462−field :: forall m t a. OutputType m t => MonadError Error m => String -> t a -> Field m a () ()
463−field name t =
464− Field
465− { name
466− , description: Nothing
467− , args: {}
468− , serialize
469− , argumentIntrospections: []
470− , typeIntrospection }
471− where
472− serialize (AST.FieldNode node) execCtx _ val = output t node.selectionSet execCtx (pure val)
473− serialize _ _ _ _ = pure $ ResultError "Obtained non FieldNode for field serialisation."
474−
475− typeIntrospection _ =
476− IntrospectionTypes.NonNullTypeIntrospection { ofType: \_ -> introspect t }
477−
478−-- | Create a new field for an object type that is a list. You can return any `Traversable` from the
479−-- | resolver.
480−listField :: forall m f t a.
481− MonadError Error m =>
482− Traversable f =>
483− OutputType m t =>
484− String ->
485− t a ->
486− Field m (f a) () ()
487−listField name t =
488− Field
489− { name
490− , description: Nothing
491− , args: {}
492− , serialize
493− , argumentIntrospections: []
494− , typeIntrospection }
495− where
496− serialize (AST.FieldNode node) execCtx _ vals = do
497− ResultList <$> fromFoldable <$> traverse (output t node.selectionSet execCtx) (pure <$> vals)
498−
499− serialize _ _ _ _ =
500− pure $ ResultError "Obtained non FieldNode for field serialisation."
501−
502− typeIntrospection _ =
503− IntrospectionTypes.NonNullTypeIntrospection
504− { ofType: \_ ->
505− IntrospectionTypes.ListTypeIntrospection { ofType: \_ -> introspect t }
506− }
507−
508−-- | Create a new field for an object type that is optional (i.e. it can be null). The resolver
509−-- | must now return a `Maybe`.
510−nullableField :: forall m t a.
511− MonadError Error m =>
512− OutputType m t =>
513− String ->
514− t a ->
515− Field m (Maybe a) () ()
516−nullableField name t =
517− Field
518− { name
519− , description: Nothing
520− , args: {}
521− , serialize
522− , argumentIntrospections: []
523− , typeIntrospection }
524− where
525− serialize (AST.FieldNode node) execCtx _ value = do
526− case value of
527− Nothing ->
528− pure $ ResultNullable Nothing
529−
530− Just val ->
531− ResultNullable <$> Just <$> output t node.selectionSet execCtx (pure val)
532−
533− serialize _ _ _ _ =
534− pure $ ResultError "Obtained non FieldNode for field serialisation."
535−
536− typeIntrospection _ = introspect t
537−
538−-- | Create a new field for an object type that is optional (i.e. it can be null) and returns a
539−-- | list. The resolver must now return a `Maybe`.
540−nullableListField :: forall m f t a.
541− MonadError Error m =>
542− Traversable f =>
543− OutputType m t =>
544− String ->
545− t a ->
546− Field m (Maybe (f a)) () ()
547−nullableListField name t =
548− Field
549− { name
550− , description: Nothing
551− , args: {}
552− , serialize
553− , argumentIntrospections: []
554− , typeIntrospection }
555− where
556− serialize (AST.FieldNode node) variables _ values = do
557− case values of
558− Nothing ->
559− pure $ ResultNullable Nothing
560−
561− Just vals ->
562− ResultNullable <$>
563− Just <$>
564− ResultList <$>
565− fromFoldable <$>
566− traverse (output t node.selectionSet variables) (pure <$> vals)
567− serialize _ _ _ _ =
568− pure $ ResultError "Obtained non FieldNode for field serialisation."
569−
570− typeIntrospection _ =
571− IntrospectionTypes.ListTypeIntrospection { ofType: \_ -> introspect t }
572−
573−-- | Create a tuple with a given name and a plain argument of the given type. Arguments are passed
574−-- | into the resolver. Arguments created with this function are required and the query fails if
575−-- | no value or a `null` value is supplied for this argument. To make an argument optional use
576−-- | `optionalArg` instead.
577−-- | The name argument tuple can then be used to be added to a field using the `?>` operator.
578−-- |
579−-- | *Example:*
580−-- | ```purescript
581−-- | objectType "Query"
582−-- | :> field "hello" Scalar.string
583−-- | ?> arg Scalar.string (SProxy :: _ "name")
584−-- | !> \{ name } parent -> pure $ "Hello " <> name
585−-- | ```
586−arg :: forall t a n. InputType t => IsSymbol n => t a -> SProxy n -> Tuple (SProxy n) (Argument a)
587−arg t name =
588− Tuple (SProxy :: _ n) $
589− Argument
590− { description: Nothing
591− , resolveValue: input t
592− , typeIntrospection: \_ -> introspect t
593− }
594−
595−-- | Create a tuple with a given name and a plain argument of the given type, that is optional.
596−-- | If the argument is supplied for the field the value is available in the arguments wrapped in
597−-- | `Just`. If the argument is not supplied or the supplied value is `null` the arguments field
598−-- | in the resolver is set to `Nothing`.
599−-- | The name argument tuple can then be used to be added to a field using the `?>` operator.
600−-- |
601−-- | *Example:*
602−-- | ```purescript
603−-- | objectType "Query"
604−-- | :> field "hello" Scalar.string
605−-- | ?> optionalArg Scalar.string (SProxy :: _ "name")
606−-- | !> (\{ name } parent -> case name of
607−-- | Just n -> pure $ "Hello " <> name
608−-- | Nothing -> pure "Hello stranger"
609−-- | )
610−-- | ```
611−optionalArg ::
612− forall t a n.
613− InputType t =>
614− IsSymbol n =>
615− t a ->
616− SProxy n ->
617− Tuple (SProxy n) (Argument (Maybe a))
618−optionalArg t name =
619− Tuple (SProxy :: _ n) $
620− Argument
621− { description: Nothing
622− , resolveValue
623− , typeIntrospection: \_ -> introspect t
624− }
625− where
626− -- I am not really happy with this function and it is a bit of a hack:
627− -- We are basically proxying the input function and prevent it from failing if null is
628− -- supplied as the variable value. It is easy to remove the Nothing case from the literal
629− -- case but hard to remove the JSON null from the possible JSON values. I think this is the
630− -- right place to implement this logic but I don't know how to remove this case completely
631− -- from the input function.
632− resolveValue Nothing execCtx = Right Nothing
633− resolveValue (Just AST.NullValueNode) execCtx = Right Nothing
634− resolveValue a@(Just (AST.VariableNode { name: AST.NameNode n})) execCtx = do
635− variableValue <- note ("Unknown variable \"" <> n.value <> "\".") $
636− lookup n.value execCtx.variables
637− if Json.isNull variableValue then pure Nothing else Just <$> input t a execCtx
638− resolveValue a execCtx = Just <$> input t a execCtx
639−
640−
641−inputObjectType :: String -> InputObjectType {}
642−inputObjectType name = InputObjectType \_ ->
643− { name
644− , description: Nothing
645− , fieldIntrospection: []
646− , input: \_ _ -> Right {}
647− }
648−
649−
650−inputField :: forall t l a. IsSymbol l => InputType t => t a -> SProxy l -> InputField l a
651−inputField inputType label = InputField
652− { name: label
653− , introspection:
654− IntrospectionTypes.InputValueIntrospection
655− { name: reflectSymbol label
656− , description: Nothing
657− , defaultValue: Nothing
658− , type: \_ -> introspect inputType
659− }
660− , input: input inputType }
661−
662−-- * DSL Combinator operators
663−
664−-- | The describe type class is used to allow adding descriptions to various parts of a GraphQL
665−-- | schema that can have a description. The `.>` operator can be used with the GraphQL object
666−-- | type DSL to easily add descriptions in various places.
667−-- |
668−-- | *Example:*
669−-- | ```purescript
670−-- | objectType "User"
671−-- | .> "A user of the product."
672−-- | :> field "id" Scalar.id
673−-- | .> "A unique identifier for this user."
674−-- | ```
675−class Describe a where
676− describe :: a -> String -> a
677−
678−infixl 8 describe as .>
679−
680−instance describeObjectType :: Describe (ObjectType m a) where
681− describe (ObjectType configFn) s =
682− ObjectType $ \_ ->
683− let
684− config = configFn unit
685− updateDescription (IntrospectionTypes.ObjectTypeIntrospection introspection) =
686− IntrospectionTypes.ObjectTypeIntrospection $
687− introspection { description = Just s }
688− updateDescription other = other
689− in
690− config { introspection = updateDescription config.introspection }
691−
692−instance describeField :: Describe (Field m a argsd argsp) where
693− describe (Field config) s = Field (config { description = Just s })
694−
695−instance describeArgument :: Describe (Argument a) where
696− describe (Argument config) s = Argument (config { description = Just s })
697−
698−instance describeArgumentTuple :: Describe (Tuple s (Argument a)) where
699− describe (Tuple p (Argument config)) s = Tuple p (Argument (config { description = Just s }))
700−
701−instance describeEnumType :: Describe (EnumType a) where
702− describe (EnumType config) s = EnumType ( config { description = Just s })
703−
704−instance describeEnumValue :: Describe (EnumValue a) where
705− describe (EnumValue config) s = EnumValue ( config { description = Just s })
706−
707−instance describeInputObjectType :: Describe (InputObjectType a) where
708− describe (InputObjectType configFn) s =
709− InputObjectType $ \_ -> (configFn unit) { description = Just s }
710−
711−instance describeInputField :: Describe (InputField s a) where
712− describe (InputField config) s =
713− InputField $ config { introspection = i }
714− where
715− { introspection: (IntrospectionTypes.InputValueIntrospection introspection)} = config
716− i = IntrospectionTypes.InputValueIntrospection $ introspection { description = Just s }
717−
718−-- | The `withField` function is used to add fields to an object type.
719−-- |
720−-- | When added to an object type the information about the field's arguments are hidden in the
721−-- | closure by being converted to the `ExecutableField` type.
722−withField :: forall m a argsd argsp.
723− ArgsDefToArgsParam argsd argsp =>
724− MonadError Error m =>
725− ObjectType m a ->
726− Field m a argsd argsp ->
727− ObjectType m a
728−withField (ObjectType objectConfigFn) fld@(
729− Field { name, description, typeIntrospection, argumentIntrospections }
730−) =
731− ObjectType updated
732− where
733− updated _ =
734− let
735− objectConfig = objectConfigFn unit
736− updatedFields = insert name (makeExecutable fld) objectConfig.fields
737−
738− fieldIntrospection = IntrospectionTypes.FieldIntrospection
739− { name
740− , description
741− , args: argumentIntrospections
742− , type: typeIntrospection
743− , deprecationReason: Nothing
744− }
745−
746− updateIntrospection (IntrospectionTypes.ObjectTypeIntrospection introspection) =
747− IntrospectionTypes.ObjectTypeIntrospection $
748− introspection { fields = Array.snoc introspection.fields fieldIntrospection }
749− updateIntrospection other = other
750−
751− updatedIntrospection = updateIntrospection objectConfig.introspection
752− in
753− objectConfig { fields = updatedFields, introspection = updatedIntrospection }
754−
755− makeExecutable :: Field m a argsd argsp -> ExecutableField m a
756− makeExecutable (Field { args, serialize: serialize' }) = ExecutableField { execute: execute' }
757− where
758− execute' :: a -> AST.SelectionNode -> ExecutionContext -> m Result
759− execute' val node@(AST.FieldNode { arguments: argumentNodes }) variables =
760− case argsFromDefinition argumentNodes variables args of
761− Right argumentValues -> serialize' node variables argumentValues val
762− Left err -> pure (ResultError err)
763− execute' _ _ _ = pure (ResultError "Unexpected non field node field execution...")
764−
765−-- | Operator for the `withField` function: Add a field to an object type.
766−-- |
767−-- | *Example:*
768−-- | ```purescript
769−-- | objectType "Query"
770−-- | :> field "hello" Scalar.string
771−-- | ```
772−infixl 5 withField as :>
773−
774−-- | A type class that contrains the relationship between defined arguments and the _argument_
775−-- | parameter of a resolver.
776−class ArgsDefToArgsParam (argsd :: # Type) (argsp :: # Type)
777− | argsd -> argsp
778− , argsp -> argsd where
779− argsFromDefinition ::
780− List AST.ArgumentNode ->
781− ExecutionContext ->
782− Record argsd ->
783− Either String (Record argsp)
784−
785−instance argsDefToArgsParamImpl ::
786− ( RL.RowToList argsd largsd
787− , RL.RowToList argsp largsp
788− , ArgsFromRows largsd largsp argsd argsp
789− , RL.ListToRow largsd argsd
790− , RL.ListToRow largsp argsp ) => ArgsDefToArgsParam argsd argsp where
791− argsFromDefinition = argsFromRows (RLProxy :: RLProxy largsd) (RLProxy :: RLProxy largsp)
792−
793−-- | ArgsDefToArgsParam that works on row lists for matching implementations
794−class ArgsFromRows
795− (largsd :: RL.RowList)
796− (largsp :: RL.RowList)
797− (argsd :: # Type)
798− (argsp :: # Type)
799− where
800− argsFromRows ::
801− RLProxy largsd ->
802− RLProxy largsp ->
803− List AST.ArgumentNode ->
804− ExecutionContext ->
805− Record argsd ->
806− Either String (Record argsp)
807−
808−instance argsFromRowsCons ::
809− ( Row.Cons l (Argument a) targsd argsd
810− , Row.Cons l a targsp argsp
811− , Row.Lacks l targsd
812− , Row.Lacks l targsp
813− , ArgsFromRows ltargsd ltargsp targsd targsp
814− , IsSymbol l ) =>
815− ArgsFromRows
816− (RL.Cons l (Argument a) ltargsd)
817− (RL.Cons l a ltargsp)
818− argsd
819− argsp
820− where
821− argsFromRows argsdProxy argspProxy nodes variables argsd =
822− let key = SProxy :: SProxy l
823− Argument argConfig = Record.get key argsd
824− tailArgsDef = Record.delete key argsd :: Record targsd
825− nameEquals (AST.ArgumentNode { name: AST.NameNode { value } }) =
826− value == reflectSymbol key
827− argumentNode = find nameEquals nodes
828− -- Extract the argument value if the node can be found. If not use null value
829− -- TODO: Implement default values where we would try to use the default value first
830− valueNode = map (\(AST.ArgumentNode { value }) -> value) argumentNode
831− improveInputError err = "Error when reading argument '" <> reflectSymbol key <> "': " <> err
832− resolvedValue = lmap improveInputError $ argConfig.resolveValue valueNode variables
833− tail =
834− argsFromRows
835− (RLProxy :: RLProxy ltargsd)
836− (RLProxy :: RLProxy ltargsp)
837− nodes
838− variables
839− tailArgsDef
840− in Record.insert key <$> resolvedValue <*> tail
841−
842−else instance argsFromRowsNil :: ArgsFromRows RL.Nil RL.Nil argsd argsp where
843− argsFromRows _ _ _ _ _ = pure $ unsafeCoerce {}
844−
845−-- | The `withArgument` function adds an argument to a field. The argument can then be used in
846−-- | resolvers that are added to the field. The old resolver of the field is still valid until it is
847−-- | overwritten by `withResolver.`
848−withArgument :: forall m a arg n argsdold argsdnew argspold argspnew.
849− IsSymbol n =>
850− Row.Cons n (Argument arg) argsdold argsdnew =>
851− Row.Cons n arg argspold argspnew =>
852− Row.Lacks n argsdold =>
853− Row.Lacks n argspold =>
854− ArgsDefToArgsParam argsdold argspold =>
855− ArgsDefToArgsParam argsdnew argspnew =>
856− Field m a argsdold argspold ->
857− Tuple (SProxy n) (Argument arg) ->
858− Field m a argsdnew argspnew
859−withArgument (Field fieldConfig) (Tuple proxy argument) =
860− Field $ fieldConfig { args = args', serialize = serialize', argumentIntrospections = intros' }
861− where
862− args' :: Record argsdnew
863− args' = Record.insert proxy argument fieldConfig.args
864−
865− serialize' ::
866− AST.SelectionNode ->
867− ExecutionContext ->
868− Record argspnew ->
869− a ->
870− m Result
871− serialize' node variables argsnew val =
872− fieldConfig.serialize node variables (Record.delete proxy argsnew) val
873−
874− intros' :: Array IntrospectionTypes.InputValueIntrospection
875− intros' = Array.snoc fieldConfig.argumentIntrospections
876− (
877− IntrospectionTypes.InputValueIntrospection
878− { name: reflectSymbol proxy
879− , description: _.description $ unwrap argument
880− , type: _.typeIntrospection $ unwrap argument
881− , defaultValue: Nothing
882− }
883− )
884−
885−-- | Adds an argument to a field that can be used in the resolver.
886−-- |
887−-- | *Example:*
888−-- | ```purescript
889−-- | objectType "Query"
890−-- | :> field "hello" Scalar.string
891−-- | ?> arg (SProxy :: SProxy "name") Scalar.string
892−-- | !> (\{ name } parent -> "Hello " <> name <> "!")
893−-- | ```
894−infixl 7 withArgument as ?>
895−
896−-- | The `withResolver` function adds a resolver to a field.
897−-- |
898−-- | _Note: When used multiple times on a field the resolvers are chained._
899−withResolver :: forall m a b argsd argsp.
900− MonadError Error m =>
901− Field m a argsd argsp ->
902− (Record argsp -> b -> m a) ->
903− Field m b argsd argsp
904−withResolver (Field fieldConfig) resolver =
905− Field $ fieldConfig { serialize = serialize }
906− where
907− serialize :: AST.SelectionNode -> ExecutionContext -> Record argsp -> b -> m Result
908− serialize node variables args = resolver args >=> fieldConfig.serialize node variables args
909−
910−-- | Add a resolver to a field that receives the arguments in a record and the parent value.
911−-- |
912−-- | *Example:*
913−-- | ```purescript
914−-- | objectType "User"
915−-- | :> field "friends" Scalar.string
916−-- | !> (\_ user -> user >>= loadUserFriends)
917−-- | ```
918−infixl 6 withResolver as !>
919−
920−-- | Add a resolver to a field that does not take the arguments. This is useful in the very common
921−-- | case that your field has no arguments.
922−withSimpleResolver :: forall m a b.
923− MonadError Error m =>
924− Field m a () () ->
925− (b -> m a) ->
926− Field m b () ()
927−withSimpleResolver fld resolver = withResolver fld (const resolver)
928−
929−-- | Add a resolver to a field that receives only the parent value and ignores the arguments.
930−-- |
931−-- | *Example:*
932−-- | ```purescript
933−-- | objectType "User"
934−-- | :> field "friends" Scalar.string
935−-- | !!> (\user -> user >>= loadUserFriends)
936−-- | ```
937−infixl 6 withSimpleResolver as !!>
938−
939−-- | Add a resolver that simply maps over the parent value. This is useful when you have a pure
940−-- | resolver that does not run effects.
941−withMappingResolver :: forall m a b.
942− MonadError Error m =>
943− Field m a () () ->
944− (b -> a) ->
945− Field m b () ()
946−withMappingResolver fld resolver = withSimpleResolver fld (resolver >>> pure)
947−
948−-- | Add a pure resolver to a field that simply maps the parent value.
949−-- |
950−-- | *Example:*
951−-- | ```purescript
952−-- | objectType "User"
953−-- | :> field "id" Scalar.string
954−-- | !#> _.id
955−-- | ```
956−infixl 6 withMappingResolver as !#>
957−
958−-- | Add a field to an input object type.
959−-- | Usually this functions operator alias
960−withInputField :: forall l a r1 r2.
961− IsSymbol l
962− => Lacks l r1
963− => Cons l a r1 r2
964− => InputObjectType { | r1 }
965− -> InputField l a
966− -> InputObjectType { | r2 }
967−withInputField (InputObjectType objConfig) (InputField inputConfig) = InputObjectType (\_ ->
968− let
969− config = (objConfig unit)
970− fieldLabel = (SProxy :: SProxy l)
971− fieldName = reflectSymbol fieldLabel
972−
973− input :: Maybe AST.ValueNode -> ExecutionContext -> Either String (Record r2)
974− input variableNode execCtx = do
975− let readField = Json.caseJsonObject (Right Nothing) (flip Json.getFieldOptional' fieldName)
976− recValue <- config.input variableNode execCtx
977− case variableNode of
978− (Just (AST.VariableNode { name: AST.NameNode n })) ->
979− case lookup n.value execCtx.variables of
980− Just json -> do
981− maybeValue <- readField json
982− let hackedVariables = insert "variable_hack" (fromMaybe Json.jsonNull maybeValue) empty
983− let hackedCtx = { fragments: execCtx.fragments, variables: hackedVariables }
984− fieldValue <-
985− inputConfig.input
986− (Just (AST.VariableNode { name: (AST.NameNode { value: "variable_hack" }) }))
987− hackedCtx
988− pure $ Record.insert fieldLabel fieldValue recValue
989− Nothing -> Left ("Unknown variable \"" <> n.value <> "\".")
990−
991− (Just (AST.ObjectValueNode { fields })) ->
992− case find (\(AST.ObjectFieldNode { name: AST.NameNode n }) -> n.value == fieldName) fields of
993− Just (AST.ObjectFieldNode { value }) -> do
994− fieldValue <- inputConfig.input (Just value) execCtx
995− pure $ Record.insert fieldLabel fieldValue recValue
996− Nothing -> Left ("Field '" <> fieldName <> "' is required in object type.")
997−
998− _ -> Left "Must provide value for required input object value."
999−
1000− fieldIntrospection = Array.cons inputConfig.introspection config.fieldIntrospection
1001−
1002− in config { input = input, fieldIntrospection = fieldIntrospection }
1003− )
1004−
1005−-- | Add a field to an input object type.
1006−-- |
1007−-- | *Example:*
1008−-- | ```purescript
1009−-- | inputObjectType "User"
1010−-- | :?> inputField "name" Scalar.string
1011−-- | .> "The name of the user"
1012−-- | ```
1013−infixl 6 withInputField as :?>
1014−
1015−-- | Take a bounded enum and infer an enum type from that value using it's show instance to
1016−-- | represent the enum string. The show function cannot return values that are not valid GraphQL
1017−-- | indentifier strings. For practical reasons this is not checked at compile time. Please refer to
1018−-- | the tutorial for best practices around enum values.
1019−enumType :: forall a. Bounded a => Enum a => Show a => String -> EnumType a
1020−enumType name = EnumType { name, description: Nothing, values }
1021− where
1022− values = enumFromTo top bottom <#> \value ->
1023− EnumValue { name: show value, description: Nothing, value, isValue: eq value }
1+module GraphQL.Type
2+ ( module Schema
3+ , module Class
4+ , module ScalarType
5+ , module ObjectType
6+ , module EnumType
7+ , module UnionType
8+ , module InputObjectType
9+ ) where
10+
11+import GraphQL.Type.Schema (Schema(..)) as Schema
12+import GraphQL.Type.Class (class GraphQLType, class InputType, class OutputType, ExecutionContext, input, introspect, output) as Class
13+import GraphQL.Type.ScalarType (ScalarType(..)) as ScalarType
14+import GraphQL.Type.ObjectType (Argument, Field, ObjectType) as ObjectType
15+import GraphQL.Type.EnumType (EnumType) as EnumType
16+import GraphQL.Type.UnionType (UnionType) as UnionType
17+import GraphQL.Type.InputObjectType (InputField(..), InputObjectType(..)) as InputObjectType
src/GraphQL/Type/Class.pursnew file+37
@@ -0,0 +1,37 @@
1+module GraphQL.Type.Class
2+ ( class GraphQLType
3+ , introspect
4+ , class InputType
5+ , input
6+ , class OutputType
7+ , output
8+ , ExecutionContext
9+ ) where
10+
11+import Control.Monad.Error.Class (class MonadError)
12+import Data.Argonaut (Json)
13+import Data.Either (Either)
14+import Data.Map (Map)
15+import Data.Maybe (Maybe)
16+import Effect.Exception (Error)
17+import GraphQL.Execution.Result (Result)
18+import GraphQL.Language.AST as AST
19+import GraphQL.Type.Introspection.Datatypes as IntrospectionTypes
20+
21+type ExecutionContext =
22+ { variables :: Map String Json
23+ , fragments :: Map String AST.DefinitionNode
24+ }
25+
26+-- | The type class for all GraphQL types
27+class GraphQLType t where
28+ introspect :: forall a. t a -> IntrospectionTypes.TypeIntrospection
29+
30+-- | The input type class is used for all GraphQL types that can be used as input types.
31+class GraphQLType t <= InputType t where
32+ input :: forall a. t a -> Maybe AST.ValueNode -> ExecutionContext -> Either String a
33+
34+-- | The output type class is used for all GraphQL types that can be used as output types.
35+-- | It has instances for GraphQL types that can resolve a value within a certain monad error.
36+class (MonadError Error m, GraphQLType t) <= OutputType m t where
37+ output :: forall a. t a -> Maybe AST.SelectionSetNode -> ExecutionContext -> m a -> m Result
src/GraphQL/Type/EnumType.pursnew file+83
@@ -0,0 +1,83 @@
1+module GraphQL.Type.EnumType
2+ ( EnumType(..)
3+ , EnumValue(..)
4+ ) where
5+
6+import Prelude
7+
8+import Control.Monad.Error.Class (class MonadError, throwError)
9+import Data.Argonaut as Json
10+import Data.Either (Either(..), note)
11+import Data.List (find)
12+import Data.Map (lookup)
13+import Data.Maybe (Maybe(..))
14+import Effect.Exception (Error, error)
15+import GraphQL.Execution.Result (Result(..))
16+import GraphQL.Language.AST as AST
17+import GraphQL.Type.Class (class GraphQLType, class InputType, class OutputType, ExecutionContext)
18+import GraphQL.Type.Introspection.Datatypes as IntrospectionTypes
19+
20+
21+newtype EnumType a =
22+ EnumType
23+ { name :: String
24+ , description :: Maybe String
25+ , values :: Array (EnumValue a)
26+ }
27+
28+
29+newtype EnumValue a =
30+ EnumValue
31+ { name :: String
32+ , description :: Maybe String
33+ , value :: a
34+ , isValue :: a -> Boolean }
35+
36+
37+instance graphqlTypeEnumType :: GraphQLType EnumType where
38+ introspect (EnumType { name, description, values }) =
39+ IntrospectionTypes.EnumTypeIntrospection
40+ { name
41+ , description
42+ , enumValues: values <#> \(EnumValue val) ->
43+ IntrospectionTypes.EnumValueIntrospection
44+ { name: val.name
45+ , description: val.description
46+ , deprecationReason: Nothing
47+ }
48+ }
49+
50+
51+instance inputTypeEnumType :: InputType EnumType where
52+ input (EnumType config) (Just node) execContext =
53+ let lookupName name =
54+ note ("Unknown enum value `" <> name <> "` for type `" <> config.name <> "`.") $
55+ find (\(EnumValue val) -> val.name == name) config.values
56+ in case node of
57+ AST.VariableNode { name: AST.NameNode { value: name } } -> do
58+ json <- note ("Required variable `" <> name <> "` was not provided.") $
59+ lookup name execContext.variables
60+ EnumValue val <- Json.caseJsonString (Left "Enum values must be strings.") lookupName json
61+ pure val.value
62+
63+ AST.EnumValueNode { name: AST.NameNode { value: name } } -> do
64+ EnumValue val <- lookupName name
65+ pure val.value
66+
67+ _ -> Left "Unexpected non-enum value node for enum value."
68+
69+ input _ _ _ = Left "Missing value for required argument."
70+
71+
72+instance outputTypeEnumType :: MonadError Error m => OutputType m EnumType where
73+ output :: forall a. EnumType a -> Maybe AST.SelectionSetNode -> ExecutionContext -> m a -> m Result
74+ output (EnumType config) Nothing _ mValue = do
75+ value <- mValue
76+ case find (\(EnumValue { isValue }) -> isValue value) config.values of
77+ Nothing ->
78+ throwError $ error "Could not select enum value representation from provided value."
79+
80+ Just (EnumValue val) ->
81+ pure $ ResultLeaf $ Json.fromString val.name
82+
83+ output _ _ _ _ = pure $ ResultError "Invalid subselection on enum type!"
src/GraphQL/Type/InputObjectType.pursnew file+49
@@ -0,0 +1,49 @@
1+module GraphQL.Type.InputObjectType where
2+
3+import Prelude
4+
5+import Control.Lazy (class Lazy)
6+import Data.Either (Either)
7+import Data.Maybe (Maybe)
8+import Data.Newtype (class Newtype, unwrap)
9+import Data.Symbol (SProxy)
10+import GraphQL.Language.AST as AST
11+import GraphQL.Type.Class (class GraphQLType, class InputType, ExecutionContext)
12+import GraphQL.Type.Introspection.Datatypes as IntrospectionTypes
13+
14+
15+newtype InputObjectType a = InputObjectType (Unit ->
16+ { name :: String
17+ , description :: Maybe String
18+ , fieldIntrospection :: Array IntrospectionTypes.InputValueIntrospection
19+ , input :: Maybe AST.ValueNode -> ExecutionContext -> Either String a
20+ }
21+)
22+
23+
24+derive instance newtypeInputObjectType :: Newtype (InputObjectType a) _
25+
26+
27+instance graphqlTypeInputObjectType :: GraphQLType InputObjectType where
28+ introspect (InputObjectType config) =
29+ IntrospectionTypes.InputObjectTypeIntrospection
30+ { name: (config unit).name
31+ , description: (config unit).description
32+ , inputFields: (config unit).fieldIntrospection
33+ }
34+
35+
36+instance inputTypeInputObjectType :: InputType InputObjectType where
37+ input (InputObjectType config) = (config unit).input
38+
39+
40+instance lazyInputObjectType :: Lazy (InputObjectType a) where
41+ defer fn = InputObjectType $ \_ -> unwrap (fn unit) unit
42+
43+
44+newtype InputField l a = InputField
45+ { name :: SProxy l
46+ , required :: Boolean
47+ , introspection :: IntrospectionTypes.InputValueIntrospection
48+ , input :: Maybe AST.ValueNode -> ExecutionContext -> Either String a
49+ }
src/GraphQL/Type/Introspection/Datatypes.purs+1
@@ -63,6 +63,7 @@ getName (EnumTypeIntrospection { name }) = Just name
6363getName (UnionTypeIntrospection { name }) = Just name
6464getName _ = Nothing
6565
66+
6667-- | Read the description of a type from a type introspection.
6768getDescription :: TypeIntrospection -> Maybe String
6869getDescription (ScalarTypeIntrospection { description }) = description
src/GraphQL/Type/Introspection/Util.purs+7−1
@@ -44,7 +44,7 @@ collectTypes (SchemaIntrospection { queryType, mutationType }) =
4444 Set TypeIntrospection
4545 collectTypesFromArg set (InputValueIntrospection config) =
4646 let
47− inputType = config.type unit
47+ inputType = getNamedType $ config.type unit
4848 in
4949 if member inputType set then set else
5050 let
@@ -57,3 +57,9 @@ collectTypes (SchemaIntrospection { queryType, mutationType }) =
5757 _ -> set
5858 in
5959 insert inputType newSet
60+
61+
62+getNamedType :: TypeIntrospection -> TypeIntrospection
63+getNamedType (ListTypeIntrospection { ofType }) = getNamedType (ofType unit)
64+getNamedType (NonNullTypeIntrospection { ofType }) = getNamedType (ofType unit)
65+getNamedType t = t
src/GraphQL/Type/ObjectType.pursnew file+128
@@ -0,0 +1,128 @@
1+module GraphQL.Type.ObjectType
2+ ( ObjectType(..)
3+ , ExecutableField(..)
4+ , Field(..)
5+ , Argument(..)
6+ ) where
7+
8+import Prelude
9+
10+import Control.Lazy (class Lazy)
11+import Control.Monad.Error.Class (class MonadError, catchError)
12+import Data.Argonaut as Json
13+import Data.Either (Either)
14+import Data.List (List, singleton)
15+import Data.Map (Map, lookup)
16+import Data.Maybe (Maybe(..), fromMaybe)
17+import Data.Newtype (class Newtype, unwrap)
18+import Data.Traversable (traverse)
19+import Data.Tuple (Tuple(..))
20+import Effect.Exception (Error, message)
21+import GraphQL.Execution.Result (Result(..))
22+import GraphQL.Language.AST as AST
23+import GraphQL.Type.Class (class GraphQLType, class OutputType, ExecutionContext)
24+import GraphQL.Type.Introspection.Datatypes as IntrospectionTypes
25+
26+
27+newtype ObjectType m a =
28+ ObjectType (Unit ->
29+ { name :: String
30+ , fields :: Map String (ExecutableField m a)
31+ , introspection :: IntrospectionTypes.TypeIntrospection
32+ }
33+ )
34+
35+
36+derive instance newtypeObjectType :: Newtype (ObjectType m a) _
37+
38+
39+instance graphqlTypeObjectType :: GraphQLType (ObjectType m) where
40+ introspect (ObjectType configFn) = (configFn unit).introspection
41+
42+
43+instance outputTypeObjectType :: (MonadError Error m) => OutputType m (ObjectType m) where
44+ output (ObjectType fno) (Just (AST.SelectionSetNode { selections })) execCtx mValue = do
45+ value <- mValue
46+ let serializeField node@(AST.FieldNode fld) =
47+ let (AST.NameNode { value: name }) = fld.name
48+ (AST.NameNode { value: alias }) = fromMaybe fld.name fld.alias
49+ o = fno unit
50+ in case lookup name o.fields of
51+ Just (ExecutableField { execute }) ->
52+ Tuple alias <$>
53+ (flip catchError (message >>> ResultError >>> pure) $
54+ execute value node execCtx)
55+ Nothing ->
56+ if
57+ name == "__typename"
58+ then
59+ pure $ Tuple alias $ ResultLeaf $ Json.fromString o.name
60+ else
61+ pure $ Tuple alias $
62+ ResultError ("Unknown field `" <> name <> "` on type `" <> o.name <> "`.")
63+ -- TODO: This branch needs fixing. It is for fragment spreads and so on. Maybe normalise
64+ -- the query first and completely eliminate the branch...
65+ serializeField _ = pure $ Tuple "unknown" $ ResultError "Unexpected fragment spread!"
66+ ResultObject <$> traverse serializeField (selections >>= collectField)
67+ where
68+ -- Flatten the selection node structure by spreading all fragments into the list
69+ collectField :: AST.SelectionNode -> List AST.SelectionNode
70+ collectField n@(AST.FieldNode _) = singleton n
71+ collectField (AST.InlineFragmentNode {
72+ selectionSet: (AST.SelectionSetNode { selections: s })
73+ }) = collectField =<< s
74+ collectField (AST.FragmentSpreadNode { name: AST.NameNode n }) =
75+ case lookup n.value execCtx.fragments of
76+ Just (AST.FragmentDefinitionNode {
77+ selectionSet: AST.SelectionSetNode { selections: s }
78+ }) -> collectField =<< s
79+ _ -> mempty
80+
81+ output _ _ _ _ = pure $ ResultError "Missing subselection on object type."
82+
83+
84+instance showObjectType :: Show (ObjectType m a) where
85+ show (ObjectType config) = "type " <> (config unit).name
86+
87+
88+instance lazyObjectType :: Lazy (ObjectType m a) where
89+ defer fn = ObjectType $ \_ -> unwrap (fn unit) unit
90+
91+
92+-- | The executable field loses the information about it's arguments types. This is needed to add it
93+-- | to the map of arguments of the object type. The execute function will extract the arguments of
94+-- | the field from the AST.
95+newtype ExecutableField m a =
96+ ExecutableField { execute :: a -> AST.SelectionNode -> ExecutionContext -> m Result }
97+
98+
99+derive instance newtypeExecutableField :: Newtype (ExecutableField m a) _
100+
101+
102+-- | Object types can have fields. Fields are constructed using the `field` function from this
103+-- | module and added to object types using the `:>` operator.
104+newtype Field m a argsd argsp =
105+ Field
106+ { name :: String
107+ , description :: Maybe String
108+ , typeIntrospection :: Unit -> IntrospectionTypes.TypeIntrospection
109+ , argumentIntrospections :: Array IntrospectionTypes.InputValueIntrospection
110+ , args :: Record argsd
111+ , serialize :: AST.SelectionNode -> ExecutionContext -> Record argsp -> a -> m Result
112+ }
113+
114+
115+derive instance newtypeField :: Newtype (Field m a argsd argsp) _
116+
117+
118+-- | Fields can have multiple arguments. Arguments are constructed using the `arg` function from
119+-- | this module and the `?>` operator.
120+newtype Argument a =
121+ Argument
122+ { description :: Maybe String
123+ , typeIntrospection :: Unit -> IntrospectionTypes.TypeIntrospection
124+ , resolveValue :: Maybe AST.ValueNode -> ExecutionContext -> Either String a
125+ }
126+
127+
128+derive instance newtypeArgument :: Newtype (Argument a) _
src/GraphQL/Type/ScalarType.pursnew file+55
@@ -0,0 +1,55 @@
1+module GraphQL.Type.ScalarType
2+ ( ScalarType(..)
3+ ) where
4+
5+
6+import Prelude
7+
8+import Control.Monad.Error.Class (class MonadError)
9+import Data.Argonaut (Json)
10+import Data.Either (Either(..), note)
11+import Data.Map (lookup)
12+import Data.Maybe (Maybe(..))
13+import Effect.Exception (Error)
14+import GraphQL.Execution.Result (Result(..))
15+import GraphQL.Language.AST as AST
16+import GraphQL.Type.Class (class GraphQLType, class OutputType, class InputType)
17+import GraphQL.Type.Introspection.Datatypes (TypeIntrospection(..))
18+
19+
20+-- | Scalar types are the leaf nodes of a GraphQL schema. Scalar types can be serialised into JSON
21+-- | or obtained from a JSON when read from the variables provided by a query. Furthermore values
22+-- | can be read from literals in a GraphQL document. To create a scalar type a name and optionally
23+-- | a description need to be provided.
24+-- | The functions `parseLiteral`, `parseValue` and `serialize` are used to convert between the
25+-- | GraphQL transport formats and PureScript representations.
26+-- |
27+-- | The GraphQL PureScript implementation comes with the five default scalars defined in the
28+-- | specification.
29+newtype ScalarType a =
30+ ScalarType
31+ { name :: String
32+ , description :: Maybe String
33+ , parseLiteral :: AST.ValueNode -> Either String a
34+ , parseValue :: Json -> Either String a
35+ , serialize :: a -> Json
36+ }
37+
38+
39+instance graphqlTypeScalarType :: GraphQLType ScalarType where
40+ introspect (ScalarType { name, description }) = ScalarTypeIntrospection { name, description }
41+
42+
43+instance inputTypeScalarType :: InputType ScalarType where
44+ input (ScalarType config) (Just node) execCtx = case node of
45+ (AST.VariableNode { name: AST.NameNode { value }}) -> do
46+ json <- note ("Required variable `" <> value <> "` was not provided.") $
47+ lookup value execCtx.variables
48+ config.parseValue json
49+ _ -> config.parseLiteral node
50+ input _ Nothing _ = Left "Must provide value for required scalar."
51+
52+
53+instance outputTypeScalarType :: (MonadError Error m) => OutputType m ScalarType where
54+ output (ScalarType s) Nothing _ val = ResultLeaf <$> map s.serialize val
55+ output _ _ _ _ = pure $ ResultError "Invalid subselection on scalar type!"
src/GraphQL/Type/Schema.pursnew file+8
@@ -0,0 +1,8 @@
1+module GraphQL.Type.Schema (Schema(..)) where
2+
3+import Data.Maybe (Maybe)
4+import GraphQL.Type.ObjectType (ObjectType)
5+
6+
7+-- | The schema contains the central entry points for GraphQL queries.
8+newtype Schema m a = Schema { query :: ObjectType m a, mutation :: Maybe (ObjectType m a) }
src/GraphQL/Type/UnionType.pursnew file+57
@@ -0,0 +1,57 @@
1+module GraphQL.Type.UnionType where
2+
3+import Prelude
4+
5+import Control.Monad.Error.Class (class MonadError)
6+import Data.List (filter)
7+import Data.Map (lookup)
8+import Data.Maybe (Maybe(..))
9+import Effect.Exception (Error)
10+import GraphQL.Execution.Result (Result)
11+import GraphQL.Language.AST as AST
12+import GraphQL.Language.AST.Util (nameFromNameNode)
13+import GraphQL.Type.Class (class GraphQLType, class OutputType, ExecutionContext)
14+import GraphQL.Type.Introspection.Datatypes as IntrospectionTypes
15+
16+
17+newtype UnionType m a = UnionType (Unit ->
18+ { name :: String
19+ , description :: Maybe String
20+ , typeIntrospections :: Array IntrospectionTypes.TypeIntrospection
21+ , output :: Maybe AST.SelectionSetNode -> ExecutionContext -> m a -> m Result
22+ }
23+)
24+
25+
26+instance graphqlTypeUnionType :: GraphQLType (UnionType m) where
27+ introspect (UnionType config) = IntrospectionTypes.UnionTypeIntrospection
28+ { name: (config unit).name
29+ , description: (config unit).description
30+ , possibleTypes: \_ -> (config unit).typeIntrospections
31+ }
32+
33+
34+-- Takes a string and only returns part of the selection set that matches the return type
35+-- TODO: Throw under certain cases, e.g. if there is a field selected in the root that is not
36+-- `__typename`
37+filterSelectionSet :: String -> AST.SelectionSetNode -> ExecutionContext -> AST.SelectionSetNode
38+filterSelectionSet typeName (AST.SelectionSetNode { selections }) ctx =
39+ AST.SelectionSetNode { selections: filter filterFn selections }
40+ where
41+ -- Only the __typename field is allowed on the root selection
42+ filterFn (AST.FieldNode { name: AST.NameNode { value }}) = value == "__typename"
43+
44+ -- If we find a fragment we only include it into the selection if it matches the type
45+ filterFn (AST.FragmentSpreadNode { name: AST.NameNode { value }}) =
46+ case lookup value ctx.fragments of
47+ Just (AST.FragmentDefinitionNode { typeCondition: AST.SimpleNamedTypeNode { name }}) ->
48+ nameFromNameNode name == typeName
49+ _ -> false
50+
51+ -- Same thing for inline fragment only here we don't have to lookup the fragment
52+ filterFn (AST.InlineFragmentNode { typeCondition: AST.SimpleNamedTypeNode { name } }) =
53+ nameFromNameNode name == typeName
54+
55+instance outputTypeUnionType :: (MonadError Error m) => OutputType m (UnionType m) where
56+ output :: forall a. UnionType m a -> Maybe AST.SelectionSetNode -> ExecutionContext -> m a -> m Result
57+ output (UnionType config) = (config unit).output
test/Execution.purs+19−21
@@ -17,10 +17,8 @@ import Data.Newtype (class Newtype, unwrap)
1717import Data.String (trim)
1818import Data.Symbol (SProxy(..))
1919import Effect.Aff (Aff, Error, throwError, error)
20−import GraphQL (graphql)
21−import GraphQL.Type ((!>), (.>), (:>), (?>), (!#>), (!!>))
22−import GraphQL.Type as GQL
23−import GraphQL.Type.Scalar as Scalar
20+import GraphQL ((!>), (.>), (:>), (?>), (!#>), (!!>))
21+import GraphQL as GQL
2422import Test.Spec (Spec, describe, it)
2523import Test.Spec.Assertions (fail, shouldEqual)
2624
@@ -62,31 +60,31 @@ queryType :: GQL.ObjectType (Either Error) String
6260queryType =
6361 GQL.objectType "Query"
6462 .> "The root query type"
65− :> GQL.field "hello" Scalar.string
63+ :> GQL.field "hello" GQL.string
6664 .> "A simple test field that connects the root string with a greeting."
6765 !> (\_ p -> pure $ "Hello " <> p <> "!")
68− :> GQL.field "greet" Scalar.string
66+ :> GQL.field "greet" GQL.string
6967 .> "A field that takes a name and responds with a presonalized greeting."
70− ?> GQL.arg Scalar.string (SProxy :: SProxy "name")
68+ ?> GQL.arg GQL.string (SProxy :: SProxy "name")
7169 !> (\{ name } _ -> pure $ "Greetings " <> name <> "!")
72− :> GQL.field "test" Scalar.int
70+ :> GQL.field "test" GQL.int
7371 !#> const 42
7472 :> GQL.field "nested" userType
7573 !#> (const $ User { id: "user1", name: "Hendrik", age: 25, level: NormalUser })
76− :> GQL.listField "someList" Scalar.string
74+ :> GQL.listField "someList" GQL.string
7775 !#> (const ["This", "is", "a", "little", "list"])
78− :> GQL.nullableField "nullable" Scalar.float
76+ :> GQL.nullableField "nullable" GQL.float
7977 !#> (const Nothing)
80− :> GQL.field "reflect" Scalar.string
78+ :> GQL.field "reflect" GQL.string
8179 ?> GQL.arg userLevelType (SProxy :: _ "argIn")
8280 !> (\{ argIn } _ -> pure $ show argIn)
83− :> GQL.field "reflectStringOptional" Scalar.string
84− ?> GQL.optionalArg Scalar.string (SProxy :: _ "argIn")
81+ :> GQL.field "reflectStringOptional" GQL.string
82+ ?> GQL.optionalArg GQL.string (SProxy :: _ "argIn")
8583 !> (\{ argIn } _ -> pure $ fromMaybe "default" argIn)
86− :> GQL.field "toggle" Scalar.boolean
87− ?> GQL.arg Scalar.boolean (SProxy :: _ "on")
84+ :> GQL.field "toggle" GQL.boolean
85+ ?> GQL.arg GQL.boolean (SProxy :: _ "on")
8886 !> (\{ on: onArg } _ -> pure $ not onArg)
89− :> GQL.nullableField "fail" Scalar.boolean
87+ :> GQL.nullableField "fail" GQL.boolean
9088 !!> (\_ -> throwError $ error "Always fails at runtime.")
9189
9290
@@ -94,11 +92,11 @@ userType :: GQL.ObjectType (Either Error) User
9492userType =
9593 GQL.objectType "User"
9694 .> "A type for all users in the database"
97− :> GQL.field "id" Scalar.string
95+ :> GQL.field "id" GQL.string
9896 !#> unwrap >>> _.id
99− :> GQL.field "name" Scalar.string
97+ :> GQL.field "name" GQL.string
10098 !#> unwrap >>> _.name
101− :> GQL.field "age" Scalar.int
99+ :> GQL.field "age" GQL.int
102100 !#> unwrap >>> _.age
103101 :> GQL.field "level" userLevelType
104102 !#> unwrap >>> _.level
@@ -112,7 +110,7 @@ userLevelType =
112110
113111testQuery :: String -> String -> Aff Unit
114112testQuery query expected =
115− case graphql testSchema query Map.empty Nothing "Hendrik" of
113+ case GQL.graphql testSchema query Map.empty Nothing "Hendrik" of
116114 Right res -> stringify res `shouldEqual` expected
117115
118116 Left message -> fail $ show message
@@ -212,5 +210,5 @@ executionSpec =
212210
213211 it "Accepts null as a variable value for an optional parameter" $ affFromEither do
214212 let query = "query ($arg: String) { reflectStringOptional(argIn: $arg) }"
215− res <- graphql testSchema query (Map.insert "arg" Json.jsonNull Map.empty) Nothing ""
213+ res <- GQL.graphql testSchema query (Map.insert "arg" Json.jsonNull Map.empty) Nothing ""
216214 stringify res `shouldEqual` """{"data":{"reflectStringOptional":"default"}}"""
test/Readme.purs+4−6
@@ -9,10 +9,8 @@ import Data.Symbol (SProxy(..))
99import Effect (Effect)
1010import Effect.Class (liftEffect)
1111import Effect.Console as Console
12−import GraphQL (graphql)
13−import GraphQL.Type ((!>), (.>), (:>), (?>))
14−import GraphQL.Type as GraphQL
15−import GraphQL.Type.Scalar as Scalar
12+import GraphQL ((!>), (.>), (:>), (?>), graphql)
13+import GraphQL as GraphQL
1614import Test.Spec (Spec, describe, it)
1715import Test.Spec.Assertions (shouldEqual)
1816
@@ -28,8 +26,8 @@ queryType :: GraphQL.ObjectType Effect Unit
2826queryType =
2927 GraphQL.objectType "Query"
3028 .> "The root query type."
31− :> GraphQL.field "hello" Scalar.string
32− ?> GraphQL.arg Scalar.string (SProxy :: SProxy "name")
29+ :> GraphQL.field "hello" GraphQL.string
30+ ?> GraphQL.arg GraphQL.string (SProxy :: SProxy "name")
3331 .> "A simple field that returns a greeting."
3432 !> (\{ name } _ -> pure $ "Hello, " <> name <> "!")
3533