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:
+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://hasura.io/learn/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:
import Arrayimport 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}}) {iduser_idis_completedtitleuser {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) Todostype alias PrivateTodo ={- todos : Todos+ todos : TodoData, visibility : String, newTodo : String}type DisplayForm= Login| Signuptype alias Model ={ privateData : PrivateTodo, publicTodoInsert : String, publicTodoInfo : PublicTodoData, online_users : OnlineUsers, authData : AuthData, authForm : AuthForm}initializePrivateTodo : PrivateTodoinitializePrivateTodo =- { 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
and GotStoredToken
messages handlers to invoke getInitialEvent
function to fetch users private todo list
GotStoredToken token ->- updateAuthData (\authData -> { authData | authToken = token }) model Cmd.none+ updateAuthData (\authData -> { authData | authToken = token }) model ( if token == "" then Cmd.none else getInitialEvent token )GotLoginResponse data ->case data ofRemoteData.Success d ->- updateAuthAndFormData (\authForm -> { authForm | isRequestInProgress = False, isSignupSuccess = False }) (\authData -> { authData | authToken = d.token }) model ( storeToken d.token )+ updateAuthAndFormData (\authForm -> { authForm | isRequestInProgress = False, isSignupSuccess = False }) (\authData -> { authData | authToken = d.token }) model ( Cmd.batch [ storeToken d.token, 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
- Build apps and APIs 10x faster
- Built-in authorization and caching
- 8x more performant than hand-rolled APIs