dillonkearns / elm-graphql / Graphql.SelectionSet

The auto-generated code from the `

Building SelectionSets

A 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)

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 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 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!

Modularizing SelectionSets

Since 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 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 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!

Mapping & Combining

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.

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 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.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 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)

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.

Pipelines

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.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 SelectionSets 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

Types

These types are built for you by the code generated by the `


type SelectionSet decodesTo scope
    = SelectionSet (List Graphql.RawField.RawField) (Json.Decode.Decoder decodesTo)

SelectionSet type


type FragmentSelectionSet decodesTo scope
    = FragmentSelectionSet String (List Graphql.RawField.RawField) (Json.Decode.Decoder decodesTo)

This type is used internally only in the generated code.

Result (...OrFail) Transformations

Warning 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?".

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.

Collections of SelectionSets

list : List (SelectionSet a scope) -> SelectionSet (List a) scope

Combine a List of SelectionSets into a single SelectionSet. Note that the SelectionSets 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 SelectionSets into a single SelectionSet. The Strings are used as the Dict keys and the result of the SelectionSets will be the values in the Dict. Note that the SelectionSets 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 SelectionSets.

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