The auto-generated code from the `
SelectionSet
sA SelectionSet
in dillonkearns/elm-graphql
represents a set of
zero or more things which are either sub-SelectionSet
s 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)
query : SelectionSet () RootQuery
query =
SelectionSet.empty
You 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.hello
This 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 SelectionSet
s 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 welcomeMessage
.
import Graphql.Operation exposing (RootQuery)
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet)
import StarWars.Query
welcomeMessage : String -> String -> String
welcomeMessage hello today =
hello ++ "\nToday is " ++ today
query : SelectionSet String RootQuery
query =
SelectionSet.map2 welcomeMessage
Query.hello
Query.today
{-
If you run this query you'll get something like:
Hello from GraphQL!
Today is Wednesday, December 5
-}
Great, 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.goodbye
Note 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!
SelectionSet
sSince both single fields and collections of fields are SelectionSet
s in dillonkearns/elm-graphql
,
you can easily pull in sub-SelectionSet
s to your queries. Just treat it like you would a regular field.
This is analogous 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 scope -> SelectionSet Posix scope
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 SelectionSet
s.
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-SelectionSet
s also allows the Elm compiler to give you more precise
error messages. Just be sure to add type annotations to all your SelectionSet
s!
Note: If you run out of mapN
functions for building up SelectionSet
s,
you can use the pipeline
which makes it easier to handle large objects, but produces
lower quality type errors.
map : (a -> b) -> SelectionSet a scope -> SelectionSet b scope
Maps the data coming back from the GraphQL endpoint. In this example,
User.name
is a function that the @dillonkearns/elm-graphql
CLI tool created which tells us
that the name
field on a User
object is a String according to your GraphQL
schema.
import Graphql.Operation exposing (RootQuery)
import Graphql.SelectionSet exposing (SelectionSet)
import StarWars.Query as Query
query : SelectionSet String RootQuery
query =
Query.hello |> SelectionSet.map String.toUpper
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 our SelectionSet
accordingly:
import Graphql.Operation exposing (RootQuery)
import Graphql.SelectionSet exposing (SelectionSet)
import StarWars.Query as Query
query : SelectionSet Int RootQuery
query =
Query.hello |> SelectionSet.map String.length
SelectionSet.map
is also helpful when using a record to wrap a type:
import Graphql.Operation exposing (RootQuery)
import Graphql.SelectionSet exposing (SelectionSet)
import StarWars.Query as Query
type alias Response =
{ hello : String }
query : SelectionSet Response RootQuery
query =
SelectionSet.map Response Query.hello
Mapping is also handy when you are dealing with polymorphic GraphQL types (Interfaces and Unions).
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet)
import StarWars.Object.Droid as Droid
import StarWars.Object.Human as Human
import StarWars.Union
import StarWars.Union.CharacterUnion
type HumanOrDroidDetails
= HumanDetails (Maybe String)
| DroidDetails (Maybe String)
heroUnionSelection : SelectionSet HumanOrDroidDetails StarWars.Union.CharacterUnion
heroUnionSelection =
StarWars.Union.CharacterUnion.fragments
{ onHuman = SelectionSet.map HumanDetails Human.homePlanet
, onDroid = SelectionSet.map DroidDetails Droid.primaryFunction
}
map2 : (decodesTo1 -> decodesTo2 -> decodesToCombined) -> SelectionSet decodesTo1 scope -> SelectionSet decodesTo2 scope -> SelectionSet decodesToCombined scope
Combine two SelectionSet
s 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.id
Check out the examples
folder, there are lots of end-to-end examples there!
map3 : (decodesTo1 -> decodesTo2 -> decodesTo3 -> decodesToCombined) -> SelectionSet decodesTo1 scope -> SelectionSet decodesTo2 scope -> SelectionSet decodesTo3 scope -> SelectionSet decodesToCombined scope
Combine three SelectionSet
s 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)
map4 : (decodesTo1 -> decodesTo2 -> decodesTo3 -> decodesTo4 -> decodesToCombined) -> SelectionSet decodesTo1 scope -> SelectionSet decodesTo2 scope -> SelectionSet decodesTo3 scope -> SelectionSet decodesTo4 scope -> SelectionSet decodesToCombined scope
map5 : (decodesTo1 -> decodesTo2 -> decodesTo3 -> decodesTo4 -> decodesTo5 -> decodesToCombined) -> SelectionSet decodesTo1 scope -> SelectionSet decodesTo2 scope -> SelectionSet decodesTo3 scope -> SelectionSet decodesTo4 scope -> SelectionSet decodesTo5 scope -> SelectionSet decodesToCombined scope
map6 : (decodesTo1 -> decodesTo2 -> decodesTo3 -> decodesTo4 -> decodesTo5 -> decodesTo6 -> decodesToCombined) -> SelectionSet decodesTo1 scope -> SelectionSet decodesTo2 scope -> SelectionSet decodesTo3 scope -> SelectionSet decodesTo4 scope -> SelectionSet decodesTo5 scope -> SelectionSet decodesTo6 scope -> SelectionSet decodesToCombined scope
map7 : (decodesTo1 -> decodesTo2 -> decodesTo3 -> decodesTo4 -> decodesTo5 -> decodesTo6 -> decodesTo7 -> decodesToCombined) -> SelectionSet decodesTo1 scope -> SelectionSet decodesTo2 scope -> SelectionSet decodesTo3 scope -> SelectionSet decodesTo4 scope -> SelectionSet decodesTo5 scope -> SelectionSet decodesTo6 scope -> SelectionSet decodesTo7 scope -> SelectionSet decodesToCombined scope
map8 : (decodesTo1 -> decodesTo2 -> decodesTo3 -> decodesTo4 -> decodesTo5 -> decodesTo6 -> decodesTo7 -> decodesTo8 -> decodesToCombined) -> SelectionSet decodesTo1 scope -> SelectionSet decodesTo2 scope -> SelectionSet decodesTo3 scope -> SelectionSet decodesTo4 scope -> SelectionSet decodesTo5 scope -> SelectionSet decodesTo6 scope -> SelectionSet decodesTo7 scope -> SelectionSet decodesTo8 scope -> SelectionSet decodesToCombined scope
withDefault : a -> SelectionSet (Maybe a) scope -> SelectionSet a scope
A helper for mapping a SelectionSet to provide a default value.
As an alternative to the mapN
functions, you can build up
SelectionSet
s 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.id
You can see an end-to-end example using the pipeline syntax in the examples
folder.
with : SelectionSet a scope -> SelectionSet (a -> b) scope -> SelectionSet b scope
See the explanation in the Pipeline section. This function is used
to add SelectionSet
s onto a pipeline.
hardcoded : a -> SelectionSet (a -> b) scope -> SelectionSet b scope
Include a hardcoded value. This is used analogously to with
to add values
into a pipeline.
import StarWars.Enum.Episode as Episode exposing (Episode)
import StarWars.Object
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet, with, hardcoded)
type alias Hero =
{ name : String
, movie : String
}
hero : SelectionSet Hero StarWars.Interface.Character
hero =
SelectionSet.succeed Hero
|> with Character.name
|> hardcoded "Star Wars"
succeed : a -> SelectionSet a scope
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
}
empty : SelectionSet () scope
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.empty
These types are built for you by the code generated by the `
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 Maybe
s 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?".
mapOrFail : (decodesTo -> Result String mapsTo) -> SelectionSet decodesTo scope -> SelectionSet mapsTo scope
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 scope -> Field Posix scope
mapToDateTime =
Field.mapOrFail
(\(Github.Scalar.DateTime value) ->
Iso8601.toTime value
|> Result.mapError (\_ -> "Failed to parse "
++ value ++ " as Iso8601 DateTime.")
nonNullOrFail : SelectionSet (Maybe decodesTo) scope -> SelectionSet decodesTo scope
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!
nonNullElementsOrFail : SelectionSet (List (Maybe decodesTo)) scope -> SelectionSet (List decodesTo) scope
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.nonNullElementsOrFail
Without the Field.nonNull...
transformations here, the type would be
SelectionSet (Maybe (List (Maybe Release))) Github.Object.ReleaseConnection
.
list : List (SelectionSet a scope) -> SelectionSet (List a) scope
Combine a List
of SelectionSet
s into a single SelectionSet
.
Note that the SelectionSet
s must first be coerced into the same type if they
are not already. Usually you'll just want to use map2
and other functions in
that section of these docs.
import Graphql.Operation exposing (RootQuery)
import Graphql.OptionalArgument exposing (OptionalArgument(..))
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet)
import Swapi.Enum.Episode as Episode exposing (Episode)
import Swapi.Interface.Character as Character
import Swapi.Query as Query
heros : SelectionSet (List String) RootQuery
heros =
Episode.list
|> List.map
(\episode ->
Query.hero
(\optionals -> { optionals | episode = Present episode })
Character.name
)
|> SelectionSet.list
dict : List ( String, SelectionSet a scope ) -> SelectionSet (Dict String a) scope
Combine several SelectionSet
s into a single SelectionSet
. The
String
s are used as the Dict
keys and the result of the SelectionSet
s
will be the values in the Dict
. Note that the SelectionSet
s must first
be coerced into the same type if they are not already. Usually you'll just
want to use map2
and other functions in that section of these docs.
import Graphql.Operation exposing (RootQuery)
import Graphql.OptionalArgument exposing (OptionalArgument(..))
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet)
import Swapi.Enum.Episode as Episode exposing (Episode)
import Swapi.Interface.Character as Character
import Swapi.Query as Query
herosDict : SelectionSet (Dict String String) RootQuery
herosDict =
Episode.list
|> List.map
(\episode ->
( Episode.toString episode
, Query.hero
(\optionals -> { optionals | episode = Present episode })
Character.name
)
)
|> SelectionSet.dict
foldl : (item -> combined -> combined) -> combined -> List (SelectionSet item scope) -> SelectionSet combined scope
Fold over each of the values in a list of SelectionSet
s.
query : SelectionSet Response RootQuery
query =
SelectionSet.foldl (+)
0
([ { owner = "dillonkearns", name = "mobster" }
, { owner = "dillonkearns", name = "elm-graphql" }
, { owner = "dillonkearns", name = "elm-typescript-interop" }
]
|> List.map (\args -> Query.repository args stargazerCount)
|> List.map SelectionSet.nonNullOrFail
)
stargazerCount : SelectionSet Int Github.Object.Repository
stargazerCount =
Repository.stargazers identity Github.Object.StargazerConnection.totalCount