The auto-generated code from the @dillonkearns/elm-graphql CLI provides
functions that you can use to build up SelectionSets for the GraphQL Objects,
Interfaces, and Unions in your GraphQL schema.
Note that in these examples, all of the modules that start with StarWars. or Github.
are generated by running the @dillonkearns/elm-graphql
command line tool.
There are lots more end-to-end examples in the
examples
folder.
With dillonkearns/elm-graphql, a SelectionSet describes a set of fields to
retrieve. It contains all the information needed to make the request and decode
the response (you don't hand-code the decoders yourself, they are auto-generated
for you!).
SelectionSetsA SelectionSet in dillonkearns/elm-graphql represents a set of
zero or more things which are either sub-SelectionSets or leaf fields.
For example, SelectionSet.empty is the most basic SelectionSet you could build.
import Graphql.Operation exposing (RootQuery)
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet)
import StarWars.Query as Query
query : SelectionSet () RootQuery
query =
SelectionSet.emptyYou can execute this query, but the result won't be very interesting!
In the StarWars API example in the examples
folder, there is a top-level query field called hello. So you could also
build a valid query to get hello:
import Graphql.Operation exposing (RootQuery)
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet)
import StarWars.Query as Query
query : SelectionSet Response RootQuery
query =
Query.helloThis is the equivalent of this raw GraphQL query:
query {
hello
}If we wanted to query for two top-level fields it's just as easy. Let's see how we would grab both the hello and goodbye fields like this:
query {
hello
goodbye
}The only difference for combining two SelectionSets is that you need to define which
function we want to use to combine the two fields together into one piece of data.
Let's just define our own function for now, called helloAndGoodbyeToString.
import Graphql.Operation exposing (RootQuery)
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet)
import StarWars.Query
helloAndGoodbyeToString : String -> String -> String
helloAndGoodbyeToString helloValue goodbyeValue =
"greeting: "
++ helloValue
++ "\ngoodbye: "
++ goodbyeValue
hero : SelectionSet String RootQuery
hero =
SelectionSet.map2 helloAndGoodbyeToString
Query.hello
Query.goodbyeGreat, we retrieved two fields! But often you don't want to combine the values
into a primitive, you just want to store the values in some data structure
like a record. So a very common pattern is to use record constructors as the
constructor function for map2 (or mapN). Any function that takes the right number of arguments
(of the right types, order matters) will work here.
Let's define a type alias for a record called Phrases. When we define this
type alias, Elm creates a function called Phrases that will build up a record
of that type. So we can use that function with map2!
import Graphql.Operation exposing (RootQuery)
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet)
import StarWars.Query
type alias Phrases =
{ helloPhrase : String
, goodbyePhrase : String
}
hero : SelectionSet Phrases RootQuery
hero =
SelectionSet.map2 Phrases
Query.hello
Query.goodbyeNote that if you changed the order of Query.hello and Query.goodbye,
you would end up with a record with values under the wrong name. Order matters
with record constructors!
SelectionSetsSince both single fields and collections of fields are SelectionSets in dillonkearns/elm-graphql,
you can easily pull in sub-SelectionSets to your queries. Just treat it like you would a regular field.
This is analagous to using a fragment in plain GraphQL. This is a handy tool for modularizing your GraphQL queries.
Let's say we want to query Github's GraphQL API like this:
{
repository(owner: "dillonkearns", name: "elm-graphql") {
nameWithOwner
...timestamps
stargazers(first: 0) { totalCount }
}
}
fragment timestamps on Repository {
createdAt
updatedAt
}(You can try the above query for yourself by pasting the query into the Github query explorer).
We could do the equivalent of the timestamps fragment with the timestampsFragment
we define below.
import Github.Object
import Github.Object.Repository as Repository
import Graphql.Operation exposing (RootQuery)
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet)
import Iso8601
import Time exposing (Posix)
type alias Repo =
{ nameWithOwner : String
, timestamps : Timestamps
}
type alias Timestamps =
{ createdAt : Posix
, updatedAt : Posix
}
repositorySelection : SelectionSet Repo Github.Object.Repository
repositorySelection =
SelectionSet.map2 Repo
Repository.nameWithOwner
timestampsFragment
timestampsFragment : SelectionSet Timestamps Github.Object.Repository
timestampsFragment =
SelectionSet.map2 Timestamps
(Repository.createdAt |> mapToDateTime)
(Repository.updatedAt |> mapToDateTime)
mapToDateTime : SelectionSet Github.Scalar.DateTime typeLock -> SelectionSet Posix typeLock
mapToDateTime =
SelectionSet.mapOrFail
(\(Github.Scalar.DateTime value) ->
Iso8601.toTime value
|> Result.mapError
(\_ ->
"Failed to parse "
++ value
++ " as Iso8601 DateTime."
)
)Note that both individual GraphQL fields (like Repository.nameWithOwner), and
collections of fields (like our timestampsFragment) are just SelectionSets.
So whether it's a single field or a pair of fields, we can pull it into our
query using the exact same syntax!
Modularizing your queries like this is a great idea. Dealing with these
sub-SelectionSets also allows the Elm compiler to give you more precise
error messages. Just be sure to add type annotations to all your SelectionSets!
Note: If you run out of mapN functions for building up SelectionSets,
you can use the pipeline
which makes it easier to handle large objects, but produces
lower quality type errors.
Combine two SelectionSets into one, using the given combine function to
merge the two data sets together.
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet)
import StarWars.Object
import StarWars.Object.Human as Human
import StarWars.Scalar
type alias Human =
{ name : String
, id : StarWars.Scalar.Id
}
hero : SelectionSet Hero StarWars.Object.Human
hero =
SelectionSet.map2 Human
Human.name
Human.idCheck out the examples
folder, there are lots of end-to-end examples there!
Combine three SelectionSets into one, using the given combine function to
merge the two data sets together. This gives more clear error messages than the
pipeline syntax (using SelectionSet.succeed to start the pipeline
and SelectionSet.with to continue it).
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet)
import StarWars.Interface
import StarWars.Interface.Character as Character
import StarWars.Scalar
type alias Character =
{ name : String
, id : StarWars.Scalar.Id
, friends : List String
}
characterSelection : SelectionSet Character StarWars.Interface.Character
characterSelection =
SelectionSet.map3 Character
Character.name
Character.id
(Character.friends Character.name)A helper for mapping a SelectionSet to provide a default value.
As an alternative to the mapN functions, you can build up
SelectionSets using the pipeline syntax. If you've used
the elm-json-decode-pipeline
package then this style will feel very familiar. The map2 example in this page
would translate to this using the pipeline notation:
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet, with)
import StarWars.Object
import StarWars.Object.Human as Human
type alias Human =
{ name : String
, id : String
}
hero : SelectionSet Hero StarWars.Object.Human
hero =
SelectionSet.succeed Human
|> with Human.name
|> with Human.idYou can see an end-to-end example using the pipeline syntax in the examples
folder.
See the explanation in the Pipeline section. This function is used
to add SelectionSets onto a pipeline.
Include a hardcoded value. This is used analagously to with to add values
into a pipeline.
import StarWars.Enum.Episode as Episode exposing (Episode)
import StarWars.Object
import Graphql.SelectionSet exposing (SelectionSet, with, hardcoded)
type alias Hero =
{ name : String
, movie : String
}
hero : SelectionSet Hero StarWars.Interface.Character
hero =
Character.commonSelection Hero
|> with Character.name
|> hardcoded "Star Wars"Most commonly succeed is used to start a pipeline. See the description
in the Pipeline section above for more.
There are other ways to use succeed. It simply takes the value you pass into it
and decodes into that value without looking at the GraphQL response (just like
Json.Decode.succeed).
So instead of hardcoding a field like hardcoded, SelectionSet.succeed hardcodes
an entire SelectionSet. This can be useful if you want hardcoded data based on
only the type when using a polymorphic type (Interface or Union).
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet, with)
import StarWars.Interface
import StarWars.Interface.Character as Character
type alias Character =
{ typename : HumanOrDroid
, name : String
}
type HumanOrDroid
= Human
| Droid
hero : SelectionSet Character StarWars.Interface.Character
hero =
SelectionSet.succeed Character
|> with heroType
|> with Character.name
heroType : SelectionSet HumanOrDroid StarWars.Interface.Character
heroType =
Character.fragments
{ onHuman = SelectionSet.succeed Human
, onDroid = SelectionSet.succeed Droid
}Useful for Mutations when you don't want any data back.
import Graphql.Operation exposing (RootMutation)
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet)
import StarWars.Mutation as Mutation
sendChatMessage : String -> SelectionSet () RootMutation
sendChatMessage message =
Mutation.sendMessage
{ message = message }
SelectionSet.emptyThese types are built for you by the code generated by the @dillonkearns/elm-graphql command line tool.
SelectionSet type
This type is used internally only in the generated code.
...OrFail) TransformationsWarning When you use these functions, you lose the guarantee that the server response will decode successfully.
These helpers, though convenient, will cause your entire decoder to fail if
it ever maps to an Err instead of an Ok Result.
If you're wondering why there are so many Maybes in your generated code,
take a look at the
FAQ question "Why are there so many Maybes in my responses? How do I reduce them?".
If the map function provided returns an Ok Result, it will map to that value.
If it returns an Err, the entire response will fail to decode.
import Time exposing (Posix)
import Github.Object
import Github.Object.Repository
import Github.Scalar
-- NOTE: Iso8601 comes from an external dependency in Elm >= 0.19:
-- https://package.elm-lang.org/packages/rtfeldman/elm-iso8601-date-strings/latest/
import Iso8601
import Graphql.SelectionSet as SelectionSet exposing (with)
type alias Timestamps =
{ created : Posix
, updated : Posix
}
timestampsSelection : SelectionSet Timestamps Github.Object.Repository
timestampsSelection =
SelectionSet.succeed Timestamps
|> with (Repository.createdAt |> mapToDateTime)
|> with (Repository.updatedAt |> mapToDateTime)
mapToDateTime : Field Github.Scalar.DateTime typeLock -> Field Posix typeLock
mapToDateTime =
Field.mapOrFail
(\(Github.Scalar.DateTime value) ->
Iso8601.toTime value
|> Result.mapError (\_ -> "Failed to parse "
++ value ++ " as Iso8601 DateTime.")Effectively turns an attribute that is String => String!, or User =>
User! (if you're not familiar with the GraphQL type language notation, learn more
here).
This will cause your entire decoder to fail if the field comes back as null. It's far better to fix your schema then to use this escape hatch!
Effectively turns a field that is [String] => [String!], or [User] =>
[User!] (if you're not familiar with the GraphQL type language notation, learn more
here).
This will cause your entire decoder to fail if any elements in the list for this field comes back as null. It's far better to fix your schema then to use this escape hatch!
Often GraphQL schemas will contain things like [String] (i.e. a nullable list
of nullable strings) when they really mean [String!]! (a non-nullable list of
non-nullable strings). You can chain together these nullable helpers if for some
reason you can't go in and fix this in the schema, for example:
releases : SelectionSet (List Release) Github.Object.ReleaseConnection
releases =
Github.Object.ReleaseConnection.nodes release
|> Field.nonNullOrFail
|> Field.nonNullElementsOrFailWithout the Field.nonNull... transformations here, the type would be
SelectionSet (Maybe (List (Maybe Release))) Github.Object.ReleaseConnection.
Maps the data coming back from the GraphQL endpoint. In this example,
User.nameis a function that the@dillonkearns/elm-graphqlCLI tool created which tells us that thenamefield on aUserobject is a String according to your GraphQL schema.You can also map to values of a different type. For example, if we use a (
String -> Int) map function, it will change the type of ourSelectionSetaccordingly:SelectionSet.mapis also helpful when using a record to wrap a type:Mapping is also handy when you are dealing with polymorphic GraphQL types (Interfaces and Unions).