GraphQL query

In this section, we will construct a GraphQL Query and integrate it with the elm app.

Configure GraphQL client

We need a client which can take a GraphQL query, make a network call and return a GraphQL response. elm-graphql package exposes a light weight http client which can be used to make GraphQL requests. Lets configure it to our requirements

Create and Open src/GraphQLClient.elm and add the following code:

githubsrc/GraphQLClient.elm
+module GraphQLClient exposing (makeGraphQLQuery)
+
+import Graphql.Http
+import Graphql.Operation exposing (RootQuery)
+import Graphql.SelectionSet as SelectionSet exposing (SelectionSet)
+
+graphql_url : String
+graphql_url =
+ "https://learn.hasura.io/graphql"
+
+
+getAuthHeader : String -> (Graphql.Http.Request decodesTo -> Graphql.Http.Request decodesTo)
+getAuthHeader token =
+ Graphql.Http.withHeader "Authorization" ("Bearer " ++ token)
+
+
+makeGraphQLQuery : String -> SelectionSet decodesTo RootQuery -> (Result (Graphql.Http.Error decodesTo) decodesTo -> msg) -> Cmd msg
+makeGraphQLQuery authToken query decodesTo =
+ query
+ |> Graphql.Http.queryRequest graphql_url
+ {-
+ queryRequest signature is of the form
+ String -> SelectionSet decodesTo RootQuery -> Request decodesTo
+ url -> SelectionSet TasksWUser RootQuery -> Request TasksWUser
+ -}
+ |> getAuthHeader authToken
+ |> Graphql.Http.send decodesTo

Import required packages

Lets import the types, utility functions generated by elm-graphql into our app and construct a GraphQL query

Create and Open src/Main.elm and add the following code:

githubsrc/Main.elm
import Array
import Browser
+import GraphQLClient exposing (makeGraphQLQuery)
+import Graphql.Http
+import Graphql.Operation exposing (RootQuery)
+import Graphql.OptionalArgument as OptionalArgument exposing (OptionalArgument(..))
+import Graphql.SelectionSet as SelectionSet exposing (SelectionSet)
+import Hasura.Enum.Order_by exposing (Order_by(..))
+import Hasura.InputObject
+ exposing
+ ( Boolean_comparison_exp
+ , Todos_bool_exp
+ , Todos_order_by
+ , buildBoolean_comparison_exp
+ , buildTodos_bool_exp
+ , buildTodos_order_by
+ )
+import Hasura.Object
+import Hasura.Object.Todos as Todos
+import Hasura.Object.Users as Users
+import Hasura.Query as Query exposing (TodosOptionalArguments)

Construct GraphQL Query

init : ( Model, Cmd Msg )
init =
( initialize
, Cmd.none
)
+---- Application logic and variables ----
+
+
+orderByCreatedAt : Order_by -> OptionalArgument (List Todos_order_by)
+orderByCreatedAt order =
+ Present <| [ buildTodos_order_by (\args -> { args | created_at = OptionalArgument.Present order }) ]
+
+
+equalToBoolean : Bool -> OptionalArgument Boolean_comparison_exp
+equalToBoolean isPublic =
+ Present <| buildBoolean_comparison_exp (\args -> { args | eq_ = OptionalArgument.Present isPublic })
+
+
+whereIsPublic : Bool -> OptionalArgument Todos_bool_exp
+whereIsPublic isPublic =
+ Present <| buildTodos_bool_exp (\args -> { args | is_public = equalToBoolean isPublic })
+
+
+todoListOptionalArgument : TodosOptionalArguments -> TodosOptionalArguments
+todoListOptionalArgument optionalArgs =
+ { optionalArgs | where_ = whereIsPublic False, order_by = orderByCreatedAt Desc }
+selectUser : SelectionSet User Hasura.Object.Users
+selectUser =
+ SelectionSet.map User
+ Users.name
+todoListSelection : SelectionSet Todo Hasura.Object.Todos
+todoListSelection =
+ SelectionSet.map5 Todo
+ Todos.id
+ Todos.user_id
+ Todos.is_completed
+ Todos.title
+ (Todos.user selectUser)
+
+
+fetchPrivateTodosQuery : SelectionSet Todos RootQuery
+fetchPrivateTodosQuery =
+ Query.todos todoListOptionalArgument todoListSelection
+
+fetchPrivateTodos : String -> Cmd Msg
+fetchPrivateTodos authToken =
+ makeGraphQLQuery authToken
+ fetchPrivateTodosQuery
+ (RemoteData.fromResult >> FetchPrivateDataSuccess)

What does this query do?

The query fetches todos with a simple condition; is_public must be false. We sort the todos descending by its created_at time according to the schema. We specify which fields we need for the todos node.

Try out this query now

Introducing query variables

fetchPrivateTodosQuery in the above snippet generates the following GraphQL query. Please note the usage of $isPublic, they are called Query Variables in GraphQL. They help in updating the value dynamically based on the requirement. The GraphQL query would fetch public todos if is_public is true and personal todos if is_public is false.

query ($isPublic: Boolean) {
todos(order_by: [{created_at: desc}], where: {is_public: {_eq: $isPublic}}) {
id
user_id
is_completed
title
user {
name
}
}
}

Great! The query is now ready, let's integrate it with our elm code. Currently we are rendering some dummy data. Let us remove this dummy data and modify our data types to hold data which is going to be retrieved asynchronously.

- todoPrivatePlaceholder : String
- todoPrivatePlaceholder =
- "This is private todo"
-
- generateTodo : String -> Int -> Todo
- generateTodo placeholder id =
- let
- isCompleted =
- id == 1
- in
- Todo id ("User" ++ String.fromInt id) isCompleted (placeholder ++ " " ++ String.fromInt id) (generateUser id)
-
- privateTodos : Todos
- privateTodos =
- List.map (generateTodo todoPrivatePlaceholder) seedIds

Modify Data Types

type alias OnlineUser =
{ id : String
, user : User
}
+type alias TodoData =
+ RemoteData (Graphql.Http.Error Todos) Todos
type alias PrivateTodo =
{
- todos : Todos
+ todos : TodoData
, visibility : String
, newTodo : String
}
type DisplayForm
= Login
| Signup
type alias Model =
{ privateData : PrivateTodo
, publicTodoInsert : String
, publicTodoInfo : PublicTodoData
, online_users : OnlineUsers
, authData : AuthData
, authForm : AuthForm
}
initializePrivateTodo : PrivateTodo
initializePrivateTodo =
- { todos = privateTodos
+ { todos = RemoteData.Loading
, visibility = "All"
, newTodo = ""
, mutateTodo = GraphQLResponse RemoteData.NotAsked
}

The above type change will capture different states of Private Todos considering the data is loaded asynchronously.

Update Msg type and update function

Our fetchPrivateTodos function will make a GraphQL query to the server and will call the corresponding function with the response data.

type Msg
= EnteredEmail String
| EnteredPassword String
| EnteredUsername String
| MakeLoginRequest
| MakeSignupRequest
| ToggleAuthForm DisplayForm
| GotLoginResponse LoginResponseParser
| GotSignupResponse SignupResponseParser
| ClearAuthToken
+ | FetchPrivateDataSuccess TodoData

Add the following to the update function

EnteredUsername name ->
updateAuthData (\authData -> { authData | username = name }) model Cmd.none
+ FetchPrivateDataSuccess response ->
+ updatePrivateData (\privateData -> { privateData | todos = response }) model Cmd.none

Add helper function to update privateData

+updatePrivateData : (PrivateTodo -> PrivateTodo) -> Model -> Cmd Msg -> ( Model, Cmd Msg )
+updatePrivateData transform model cmd =
+ ( { model | privateData = transform model.privateData }, cmd )

Modify Private Todos Render Function

Lets modify our render functions to accept modified types

- renderTodos : PrivateTodo -> Html Msg
- renderTodos privateData =
- div [ class "tasks_wrapper" ]
- [ todoListWrapper privateData.visibility privateData.todos ]
+renderTodos : PrivateTodo -> Html Msg
+renderTodos privateData =
+ div [ class "tasks_wrapper" ] <|
+ case privateData.todos of
+ RemoteData.NotAsked ->
+ [ text "" ]
+
+ RemoteData.Success todos ->
+ [ todoListWrapper privateData.visibility todos ]
+
+ RemoteData.Loading ->
+ [ span [ class "loading_text" ]
+ [ text "Loading todos ..." ]
+ ]
+
+ RemoteData.Failure err ->
+ [ text "Error loading todos" ]

Lets wire things up, in elm an asynchronous call is called a side effect. Side effects are performed using Commands

Lets configure our init function to execute a command post successful login to fetch user's private todo list.

+getInitialEvent : String -> Cmd Msg
+getInitialEvent authToken =
+ Cmd.batch
+ [ fetchPrivateTodos authToken
+ ]
init : ( Model, Cmd Msg )
init =
( initialize
, Cmd.none
)

Lets modify our GotLoginResponse message handler to invoke getInitialEvent function to fetch users private todo list

GotLoginResponse data ->
case data of
RemoteData.Success d ->
- updateAuthAndFormData (\authForm -> { authForm | isRequestInProgress = False, isSignupSuccess = False }) (\authData -> { authData | authToken = d.token }) model Cmd.none
+ updateAuthAndFormData (\authForm -> { authForm | isRequestInProgress = False, isSignupSuccess = False }) (\authData -> { authData | authToken = d.token }) model (getInitialEvent d.token)
RemoteData.Failure err ->
updateAuthFormData (\authForm -> { authForm | isRequestInProgress = False, requestError = "Unable to authenticate you" }) model Cmd.none
_ ->
( model, Cmd.none )

Woot! You have written your first GraphQL integration with Elm. Easy isn't it?

How does this work?

Here is the summary of how this works

  • User logs in to the app
  • A command is executed which makes a GraphQL query to fetch the list of private todos
  • Whether the request is succeeds or fails, the function will invoke the configured function with the response data
  • Our update function stores the given response into our model
  • The render function picks up the updated data and handles accordingly - It can be in one of the following states: Loading, Failure, Success