GithubHelp home page GithubHelp logo

dillonkearns / elm-graphql Goto Github PK

View Code? Open in Web Editor NEW
777.0 10.0 110.0 10.2 MB

Autogenerate type-safe GraphQL queries in Elm.

Home Page: https://package.elm-lang.org/packages/dillonkearns/elm-graphql/latest

License: BSD 3-Clause "New" or "Revised" License

Elm 88.02% JavaScript 0.02% TypeScript 0.69% Shell 0.08% HTML 11.20%
elm graphql

elm-graphql's Introduction

dillonkearns/elm-graphql

Build Status Elm package npm

elm-graphql logo

Why use this package over the other available Elm GraphQL packages? This is the only one that generates type-safe code for your entire schema. Check out this blog post, Type-Safe & Composable GraphQL in Elm, to learn more about the motivation for this library. (It's also the only type-safe library with Elm 0.18 or 0.19 support, see this discourse thread).

I built this package because I wanted to have something that:

  1. Gives you type-safe GraphQL queries (if it compiles, it's valid according to the schema),
  2. Creates decoders for you in a seamless and failsafe way, and
  3. Eliminates GraphQL features in favor of Elm language constructs where possible for a simpler UX (for example, GraphQL variables & fragments should just be Elm functions, constants, lets).

See an example in action on Ellie. See more end-to-end example code in the examples/ folder.

Overview

dillonkearns/elm-graphql is an Elm package and accompanying command-line code generator that creates type-safe Elm code for your GraphQL endpoint. You don't write any decoders for your API with dillonkearns/elm-graphql, instead you simply select which fields you would like, similar to a standard GraphQL query but in Elm. For example, this GraphQL query

query {
  human(id: "1001") {
    name
    homePlanet
  }
}

would look like this in dillonkearns/elm-graphql (the code in this example that is prefixed with StarWars is auto-generated)

import Graphql.Operation exposing (RootQuery)
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet)
import StarWars.Object
import StarWars.Object.Human as Human
import StarWars.Query as Query
import StarWars.Scalar exposing (Id(..))


query : SelectionSet (Maybe HumanData) RootQuery
query =
    Query.human { id = Id "1001" } humanSelection


type alias HumanData =
    { name : String
    , homePlanet : Maybe String
    }


humanSelection : SelectionSet HumanData StarWars.Object.Human
humanSelection =
    SelectionSet.map2 HumanData
        Human.name
        Human.homePlanet

GraphQL and Elm are a perfect match because GraphQL is used to enforce the types that your API takes as inputs and outputs, much like Elm's type system does within Elm. elm-graphql simply bridges this gap by making your Elm code aware of your GraphQL server's schema. If you are new to GraphQL, graphql.org/learn/ is an excellent way to learn the basics.

After following the installation instructions to install the @dillonkearns/elm-graphql NPM package and the proper Elm packages (see the Setup section for details). Once you've installed everything, running the elm-graphql code generation tool is as simple as this:

npx @dillonkearns/elm-graphql https://elm-graphql.onrender.com --base StarWars --output examples/src

If headers are required, such as a Bearer Token, the --header flag can be supplied.

npx @dillonkearns/elm-graphql https://elm-graphql.onrender.com --base StarWars --output examples/src --header 'headerKey: header value'

Learning Resources

  • There is a thorough tutorial in the SelectionSet docs. SelectionSets are the core concept in this library, so I recommend reading through the whole page (it's not very long!).

  • The examples/ folder is another great place to start.

  • If you want to learn more GraphQL basics, this is a great tutorial, and a short read: graphql.org/learn/

  • My Elm Conf 2018 talk goes into the philosophy behind dillonkearns/elm-graphql

Types Without Borders Elm Conf Talk

(Skip to 13:06 to go straight to the dillonkearns/elm-graphql demo).

  • My 10-minute video tutorial on how to leverage Custom Scalars in elm-graphql using the Scalar Codecs feature. Scalar Codecs Tutorial

If you're wondering why code is generated a certain way, you're likely to find an answer in the Frequently Asked Questions (FAQ).

There's a very helpful group of people in the #graphql channel in the Elm Slack. Don't hesitate to ask any questions about getting started, best practices, or just general GraphQL in there!

Setup

dillonkearns/elm-graphql generates Elm code that allows you to build up type-safe GraphQL requests. Here are the steps to setup dillonkearns/elm-graphql.

  1. Add the dillonkearns/elm-graphql elm package as a dependency in your elm.json. You will also need to make sure that elm/json is a dependency of your project since the generated code has lots of JSON decoders in it.

    elm install dillonkearns/elm-graphql
    elm install elm/json
  2. Install the @dillonkearns/elm-graphql command line tool through npm. This is what you will use to generate Elm code for your API. It is recommended that you save the @dillonkearns/elm-graphql command line tool as a dev dependency so that everyone on your project is using the same version.

    npm install --save-dev @dillonkearns/elm-graphql
    # you can now run it locally using `npx elm-graphql`,
    # or by calling it through an npm script as in this project's package.json
  3. Run the @dillonkearns/elm-graphql command line tool installed above to generate your code. If you used the --save-dev method above, you can simply create a script in your package.json like the following:

    {
      "name": "star-wars-elm-graphql-project",
      "version": "1.0.0",
      "scripts": {
        "api": "elm-graphql https://elm-graphql.onrender.com/api --base StarWars"
      }
    
  4. With the above in your package.json, running npm run api will generate dillonkearns/elm-graphql code for you to call in ./src/StarWars/. You can now use the generated code as in this Ellie example or in the examples folder.

Subscriptions Support

You can do real-time APIs using GraphQL Subscriptions and dillonkearns/elm-graphql. Just wire in the framework-specific JavaScript code for opening the WebSocket connection through a port. Here's a live demo and its source code. The demo server is running Elixir/Absinthe.

Contributors

Thank you Mario Martinez (martimatix) for all your feedback, the elm-format PR, and for the incredible logo design!

Thank you Mike Stock (mikeastock) for setting up Travis CI!

Thanks for the reserved words pull request @madsflensted!

A huge thanks to @xtian for doing the vast majority of the 0.19 upgrade work! 🎉

Thank you Josh Adams (@knewter) for the code example for Subscriptions with Elixir/Absinthe wired up through Elm ports!

Thank you Romario for adding OptionalArgument.map!

Thank you Aaron White for your pull request to improve the performance and stability of the elm-format step! 🎉

Roadmap

All core features are supported. That is, you can build any query or mutation with your dillonkearns/elm-graphql-generated code, and it is guaranteed to be valid according to your server's schema.

dillonkearns/elm-graphql will generate code for you to generate subscriptions and decode the responses, but it doesn't deal with the low-level details for how to send them over web sockets. To do that, you will need to use custom code or a package that knows how to communicate over websockets (or whichever protocol) to setup a subscription with your particular framework. See this discussion for why those details are not handled by this library directly.

I would love to hear feedback if you are using GraphQL Subscriptions. In particular, I'd love to see live code examples to drive any improvements to the Subscriptions design. Please ping me on Slack, drop a message in the #graphql channel, or open up a Github issue to discuss!

I would like to investigate generating helpers to make pagination simpler for Connections (based on the Relay Cursor Connections Specification). If you have ideas on this chime in on this thread.

See the full roadmap on Trello.

elm-graphql's People

Contributors

aaronwhite avatar anagrius avatar anton-4 avatar cameron avatar cllns avatar dependabot-preview[bot] avatar dependabot-support avatar dillonkearns avatar hariroshan avatar janwirth avatar jfmengels avatar jouderianjr avatar kyasu1 avatar lydell avatar martimatix avatar maxim-filimonov avatar mcasper avatar mikeastock avatar mthadley avatar nadinda avatar neslinesli93 avatar rkashapov avatar rlopzc avatar siriusstarr avatar sporto avatar teresy avatar th3coop avatar xtian avatar yonigibbs avatar zeldaiv avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

elm-graphql's Issues

Encode.float is missing

There is Encode.int/string/etc. but not float =)

It's a simple matter of adding:

{-| Encode an float
-}
float : Float -> Value
float value =
    Json.Encode.float value
        |> Json

to Graphqelm/Encode.elm.

Allow name of API to be set from CLI

This is a feature request for allowing users of graphqelm CLI to set the name of the API with an optional flag.

For example: $ graphqelm https://graphqelm.herokuapp.com/api --name swapi

This would apply the --name argument to:

  1. The top directory of the folder structure. In the example above, the top level folder would become Swapi as opposed to Api.
  2. Each module generated by graphqelm. The module declaration would be appropriately namespaced. e.g. module Swapi.Enum.Episode exposing (..)

GraphQL Error on HTTP 200 and correct json schema wrapped in error

I'm using example from https://github.com/actix/actix-web/tree/v0.4.10/examples/juniper

$ npx graphqelm http://127.0.0.1:8080/graphql
Fetching GraphQL schema...
{ error: '{"data":{"__schema":{"queryType":{"name":"QueryRoot"},"mutationType":{"name":"MutationRoot"},"subscriptionType":null,"types":[{"kind":"OBJECT","name":"QueryRoot","description":null,"fields":[{"name":"human","description":null,"args":[{"name":"id","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Human","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Boolean","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__InputValue","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"type","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"defaultValue","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"String","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Field","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"args","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"type","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"__TypeKind","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"SCALAR","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INTERFACE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"UNION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"ENUM","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INPUT_OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"LIST","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"NON_NULL","description":null,"isDeprecated":false,"deprecationReason":null}],"possibleTypes":null},{"kind":"OBJECT","name":"MutationRoot","description":null,"fields":[{"name":"createHuman","description":null,"args":[{"name":"newHuman","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"INPUT_OBJECT","name":"NewHuman","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Human","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Type","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"kind","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"__TypeKind","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"fields","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":"false"}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Field","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"ofType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"inputFields","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"interfaces","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"possibleTypes","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"enumValues","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":"false"}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__EnumValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Schema","description":null,"fields":[{"name":"types","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"queryType","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"mutationType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"subscriptionType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"directives","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Directive","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__EnumValue","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"Episode","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"NEW_HOPE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"EMPIRE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"JEDI","description":null,"isDeprecated":false,"deprecationReason":null}],"possibleTypes":null},{"kind":"ENUM","name":"__DirectiveLocation","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"QUERY","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MUTATION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FIELD","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FRAGMENT_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FRAGMENT_SPREAD","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INLINE_SPREAD","description":null,"isDeprecated":false,"deprecationReason":null}],"possibleTypes":null},{"kind":"INPUT_OBJECT","name":"NewHuman","description":"A humanoid creature in the Star Wars universe","fields":null,"inputFields":[{"name":"name","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null},{"name":"appearsIn","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Episode","ofType":null}}}},"defaultValue":null},{"name":"homePlanet","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Directive","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"locations","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"__DirectiveLocation","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"args","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"onOperation","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":true,"deprecationReason":"Use the locations array instead"},{"name":"onFragment","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":true,"deprecationReason":"Use the locations array instead"},{"name":"onField","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":true,"deprecationReason":"Use the locations array instead"}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Human","description":"A humanoid creature in the Star Wars universe","fields":[{"name":"id","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"appearsIn","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Episode","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"homePlanet","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null}]}}}',
  status: 200 }

Running npx graphqelm --introspection-file schema.json with json extracted from output, graphqelm creates elm files in src.

Also running https://github.com/dillonkearns/graphqelm/blob/master/src/introspection-query.ts in http://127.0.0.1:8080/graphiql returns json schema that also allows to generate elm files.

More debug:

Fetching GraphQL schema...
{ Error: GraphQL Error (Code: 200): {"response":{"error":"{\"data\":{\"__schema\"

Send Example Not Working

Hi!

I wanted to upgrade my version of GraphQelm, and got stuck with your example.

import Graphqelm.Http
import Graphqelm.OptionalArgument exposing (OptionalArgument(Null, Present))
import RemoteData exposing (RemoteData)

type Msg
    = GotResponse RemoteData Graphqelm.Http.Error Response

makeRequest : Cmd Msg
makeRequest =
    query
        |> Graphqelm.Http.queryRequest "https://graphqelm.herokuapp.com/"
        |> Graphqelm.Http.withHeader "authorization" "Bearer abcdefgh12345678"
        -- If you're not using remote data, it's just
        -- |> Graphqelm.Http.send GotResponse
        -- With remote data, it's as below
        |> Graphqelm.Http.send (RemoteData.fromResult >> GotResponse)

I don't even see how it could work. Actually, RemoteData.fromResult will have a signature of Result (Graphelm.Http.Error decodesTo) decodesTo -> RemoteData (Graphqelm.Http.Error decodesTo) decodesTo.

In our application, we were doing a transformation like Result Graphqelm.Http.Error Response -> RemoteData Graphqelm.Http.Error Layout and sending directly the RemoteData in the message. Now, we need to have messages able to deal with RemoteData (Graphqelm.Http.Error decodesTo) Layout.

Is this the desired behavior? Because it looks like creating noise in the messages, like having to deal with Graphqelm.Http.Error thing rather than just Graphqelm.Http.Error.

I maybe missed something so if you can enlighten me! 😄

Unable to initialize phoenix absinthe socket connection

I have been comparing the messages sent from the phoenix javascript lib and this lib. The init message fails (doesn't match) deep in phoenix..

(FunctionClauseError) no function clause matching in Phoenix.Socket.Message.from_map!/1
            (phoenix) lib/phoenix/socket/message.ex:22: Phoenix.Socket.Message.from_map!([nil, "1", "__absinthe__:control", "phx_join", %{}]

I'm thinking it's because this line:
https://github.com/dillonkearns/graphqelm/blob/c240c755f62f9ecaa7b68ae11820da0048048239/src/Graphqelm/Subscription/Protocol.elm#L89
encodes null instead of "1". I notice your code comments indicate the "1" in that position too.

Your subscriptions support looks promising so far, although as an Elm noob, I find the abstractions a little hard to follow.

How to provide an attribute as constant

I have a record like:

type alias Thing =
   { id: String
    , name: String
    , isBusy: Bool
   }

id and name come from the Graphql api. But isBusy is made up in the FE so we can provide feedback to the user.

I don't think there is a way to add this constant atm, is there? e.g.

|> with (Thing.id)
|> with (Thing.name)
|> with (????)

Maybe Graphqelm.SelectionSet is missing a constant function?

|> with (Thing.id)
|> with (Thing.name)
|> constant False

Feedback while generating files

I just ran graphqelm on the GitHub and it took about a minute to generate the files. For a one minute wait, some users might be think: "Is this thing working?"

Therefore, it would be nice to give some sort of feedback while files are being generated.

Name

If I were searching the package website for a graphql package and saw these search results...

  1. jamesmacaulay/elm-graphql
  2. pro100filipp/elm-graphql
  3. ghivert/elm-graphql
  4. Xerpa/elm-graphql
  5. chrisalmeida/graphqelm
  6. dillonkearns/graphqelm

...my familiarity with the Elm ecosystem leads me to assume the first 4 are the serious options, and the last 2 are not worth investigating. That's a shame, because you've clearly put a ton of thought into this!

The thing is, I've had one bad experience after another with packages that don't follow Elm's "give it an obvious name" guideline. Cute names seem most often to be chosen because the author is an Elm novice coming from a JS background, where there's one shared global npm namespace instead of a per-author namespace, and "branded libraries" are the norm.

Naturally, packages created by folks who are new to Elm tend to have a lot of room for improvement!

Have you considered calling this dillonkearns/elm-graphql instead? Based on my experience, I suspect that you'd see more experienced Elm users discovering the library and giving it a shot.

graphqelm CLI throws error

When I install the graphqelm module, and run graphqelm, I get the following error:

module.js:440
    throw err;
    ^

Error: Cannot find module '../dist/bundle.js'
    at Function.Module._resolveFilename (module.js:438:15)
    at Function.Module._load (module.js:386:25)
    at Module.require (module.js:466:17)
    at require (internal/module.js:20:19)
    at Object.<anonymous> (/Users/martimatix/.nvm/versions/node/v6.1.0/lib/node_modules/graphqelm/bin/graphqelm:3:1)
    at Module._compile (module.js:541:32)
    at Object.Module._extensions..js (module.js:550:10)
    at Module.load (module.js:456:32)
    at tryModuleLoad (module.js:415:12)
    at Function.Module._load (module.js:407:3)

Generating based on local schema file instead of URL

We have a local graphql schema file generated from the server code.
I would prefer not having to spin up a server instance as part of our CI build, and
just point graphqelm at the generated schema file.

I already tried graphqelm file:///path/api.graphql

But alas not that lucky.

graphqelm crashes when using with Prisma API

Another service of www.graph.cool is Prisma https://www.prismagraphql.com/
Unfortunately it is currently not possible to use graphqelm with a Prisma-generated API, a small example can be seen here: https://eu1.prisma.sh/public-skywing-662/hello-world5/dev
When running graphqelm https://eu1.prisma.sh/public-skywing-662/hello-world5/dev --base ApiTestDeployed I get the following error message:

                                                                                                                                                                                                
-- SYNTAX PROBLEM ------------------------------------------------------ <STDIN>                                                                                                                
                                                                                                                                                                                                
I ran into something unexpected when parsing your code!                                                                                                                                         
                                                                                                                                                                                                
40� updatePost : { data : ApiTest.InputObject.PostUpdateInput.PostUpdateInput, where : ApiTest.InputObject.PostWhereUniqueInput.PostWhereUniqueInput } -> Selection                             
Set decodesTo ApiTest.Object.Post -> Field (Maybe decodesTo) RootMutation                                                                                                                       
                   ^                                                                                                                                                                            
ERRORS                                                                                                                                                                                          
                                                                                                                                                                                                
                                                                                                                                                                                                
I am looking for one of the following things:                                                                                                                                                   
                                                                                                                                                                                                
    a closing bracket '}'                                                                                                                                                                       
    whitespace                                                                                                                                                                                  
                                                                                                                                                                                                
                                                                                                                                                                                                
                                                                                                                                                                                                
elm-format process exited with code 1.                                                                                                                                                          
Was attempting to write to path src\ApiTest\Mutation.elm with contents:                                                                                                                         
-- Do not manually edit this file, it was auto-generated by Graphqelm                                                                                                                           
-- https://github.com/dillonkearns/graphqelm                                                                                                                                                    
module ApiTest.Mutation exposing (..)                                                                                                                                                           
                                                                                                                                                                                                
import Graphqelm.Internal.Builder.Argument as Argument exposing (Argument)                                                                                                                      
import Graphqelm.Field as Field exposing (Field)                                                                                                                                                
import ApiTest.Object                                                                                                                                                                           
import ApiTest.Interface                                                                                                                                                                        
import ApiTest.Union                                                                                                                                                                            
import ApiTest.Scalar                                                                                                                                                                           
import Graphqelm.OptionalArgument exposing (OptionalArgument(Absent))                                                                                                                           
import Graphqelm.Internal.Builder.Object as Object                                                                                                                                              
import Graphqelm.SelectionSet exposing (SelectionSet)                                                                                                                                           
import Graphqelm.Operation exposing (RootMutation)                                                                                                                                              
import Json.Decode as Decode exposing (Decoder)                                                                                                                                                 
import Graphqelm.Internal.Encode as Encode exposing (Value)                                                                                                                                     
import ApiTest.InputObject.PostCreateInput                                                                                                                                                      
import ApiTest.InputObject.PostUpdateInput                                                                                                                                                      
import ApiTest.InputObject.PostWhereUniqueInput                                                                                                                                                 
import ApiTest.InputObject.PostWhereUniqueInput                                                                                                                                                 
import ApiTest.InputObject.PostWhereUniqueInput                                                                                                                                                 
import ApiTest.InputObject.PostCreateInput                                                                                                                                                      
import ApiTest.InputObject.PostUpdateInput                                                                                                                                                      
import ApiTest.InputObject.PostUpdateInput                                                                                                                                                      
import ApiTest.InputObject.PostWhereInput                                                                                                                                                       
import ApiTest.InputObject.PostWhereInput                                                                                                                                                       
                                                                                                                                                                                                
                                                                                                                                                                                                
{-| Select fields to build up a top-level mutation. The request can be sent with                                                                                                                
functions from `Graphqelm.Http`.                                                                                                                                                                
-}                                                                                                                                                                                              
selection : (a -> constructor) -> SelectionSet (a -> constructor) RootMutation                                                                                                                  
selection constructor =                                                                                                                                                                         
    Object.selection constructor                                                                                                                                                                
createPost : { data : ApiTest.InputObject.PostCreateInput.PostCreateInput } -> SelectionSet decodesTo ApiTest.Object.Post -> Field decodesTo RootMutation                                       
createPost requiredArgs object =                                                                                                                                                                
      Object.selectionField "createPost" [ Argument.required "data" requiredArgs.data (ApiTest.InputObject.PostCreateInput.encode) ] (object) (identity)                                        
                                                                                                                                                                                                
                                                                                                                                                                                                
updatePost : { data : ApiTest.InputObject.PostUpdateInput.PostUpdateInput, where : ApiTest.InputObject.PostWhereUniqueInput.PostWhereUniqueInput } -> SelectionSet decodesTo ApiTest.Object.Post
 -> Field (Maybe decodesTo) RootMutation                                                                                                                                                        
updatePost requiredArgs object =                                                                                                                                                                
      Object.selectionField "updatePost" [ Argument.required "data" requiredArgs.data (ApiTest.InputObject.PostUpdateInput.encode), Argument.required "where" requiredArgs.where (ApiTest.InputO
bject.PostWhereUniqueInput.encode) ] (object) (identity >> Decode.maybe)                                                                                                                        
                                                                                                                                                                                                
                                                                                                                                                                                                
deletePost : { where : ApiTest.InputObject.PostWhereUniqueInput.PostWhereUniqueInput } -> SelectionSet decodesTo ApiTest.Object.Post -> Field (Maybe decodesTo) RootMutation                    
deletePost requiredArgs object =                                                                                                                                                                
      Object.selectionField "deletePost" [ Argument.required "where" requiredArgs.where (ApiTest.InputObject.PostWhereUniqueInput.encode) ] (object) (identity >> Decode.maybe)                 
                                                                                                                                                                                                
                                                                                                                                                                                                
upsertPost : { where : ApiTest.InputObject.PostWhereUniqueInput.PostWhereUniqueInput, create : ApiTest.InputObject.PostCreateInput.PostCreateInput, update : ApiTest.InputObject.PostUpdateInput
.PostUpdateInput } -> SelectionSet decodesTo ApiTest.Object.Post -> Field decodesTo RootMutation                                                                                                
upsertPost requiredArgs object =                                                                                                                                                                
      Object.selectionField "upsertPost" [ Argument.required "where" requiredArgs.where (ApiTest.InputObject.PostWhereUniqueInput.encode), Argument.required "create" requiredArgs.create (ApiTe
st.InputObject.PostCreateInput.encode), Argument.required "update" requiredArgs.update (ApiTest.InputObject.PostUpdateInput.encode) ] (object) (identity)                                       
                                                                                                                                                                                                
                                                                                                                                                                                                
updateManyPosts : { data : ApiTest.InputObject.PostUpdateInput.PostUpdateInput, where : ApiTest.InputObject.PostWhereInput.PostWhereInput } -> SelectionSet decodesTo ApiTest.Object.BatchPayloa
d -> Field decodesTo RootMutation                                                                                                                                                               
updateManyPosts requiredArgs object =                                                                                                                                                           
      Object.selectionField "updateManyPosts" [ Argument.required "data" requiredArgs.data (ApiTest.InputObject.PostUpdateInput.encode), Argument.required "where" requiredArgs.where (ApiTest.I
nputObject.PostWhereInput.encode) ] (object) (identity)                                                                                                                                         
                                                                                                                                                                                                
                                                                                                                                                                                                
deleteManyPosts : { where : ApiTest.InputObject.PostWhereInput.PostWhereInput } -> SelectionSet decodesTo ApiTest.Object.BatchPayload -> Field decodesTo RootMutation                           
deleteManyPosts requiredArgs object =                                                                                                                                                           
      Object.selectionField "deleteManyPosts" [ Argument.required "where" requiredArgs.where (ApiTest.InputObject.PostWhereInput.encode) ] (object) (identity)                                  

Duplicate generated function names

We have this in our ruby definition:

field :year_budget, types.Int, deprecation_reason: "Use yearBudget"
field :yearBudget, types.Int, property: :year_budget

Which generates:

{
  "name": "yearBudget",
  ...
 },
 {
    "name": "year_budget",
      ...
}

graphqelm generates:

yearBudget : FieldDecoder (Maybe Int) Api.Object.Client
yearBudget =
    Object.fieldDecoder "yearBudget" [] (Decode.int |> Decode.maybe)

yearBudget : FieldDecoder (Maybe Int) Api.Object.Client
yearBudget =
    Object.fieldDecoder "year_budget" [] (Decode.int |> Decode.maybe)

So two functions with the same name. I think that graphqelm needs to make a distinction between camelCase and snake_case somehow. Thanks.

Version number for CLI

As I updated graphqelm this morning, I wanted to check which version I had currently installed.

$ graphqelm --version or simply listing the version in the help section would be handy.

Use intelligent base primitive for scalars

As discussed in #37, the Graphql spec is actively working on a proposal to have a "serializes as" field for scalars in the introspection schema. This would allow this library's generated code to know whether to expect the scalar response to be sending back a Float or Int, etc., and would allow it to send that type when arguments of a given scalar type are passed to the server.

Currently there's nothing to be done on the Elm side because no servers implement this. But I'd like to add this functionality as soon as an implementation is available, and I'd like to track the progress of server implementations here. @xtian is considering contributing to or requesting an Elixir implementation.

Type aliases for optional and required arguments

In queries the generator adds signatures for optional arguments like:

viewer : ({ adjustments : OptionalArgument (List Api.Enum.Foo), ...) } -> { adjustments : OptionalArgument (List Api.Enum.Foo), ...) -> ... Field decodesTo Api.Object.Root

We have a function that use in many places that will take a record we have and populate the correct optional arguments:

optionalArgumentsForFilter  filter optionalArgs =
     {  optionalArgs | adjustments = .... }

So we can use this like:

|> with (Api.Object.Root.viewer  (optionalArgumentsForFilter filter) requiredArgs viewer

To have a nice type signature in optionalArgumentsForFilter we need to duplicate code from the generated code:

type alias OptionalArgs = 
   { adjustments : OptionalArgument (List Api.Enum.Foo), ...) }

optionalArgumentsForFilter  : Filter -> OptionalArgs -> OptionalArgs

This creates repetition and breaks when the generated code changes.
It would be nicer if the generator created type aliases so they can be imported.

import Api.Object.Root exposing (ViewerOptionalArgs)

optionalArgumentsForFilter  : Filter -> ViewerOptionalArgs -> ViewerOptionalArgs

HTTP interface doesn't handle errors correctly

Graphqelm.Http decodes data and error responses using Json.Decode.oneOf with the data field handled first. This swallows errors in a response like this where someField is nullable:

{
  "data": {"someField": null},
  "errors": [{"path": ["someField"], "message": "Not Found"}]
}

Maybe an interface like this would work?

send : (Result HttpError (List GraphQlError, a) -> msg) -> Request (List GraphQlError, a) -> Cmd msg

toTask : Request (List GraphQlError, decodesTo) -> Task HttpError (List GraphQlError, decodesTo)

The other use-case to consider here is an operation with multiple queries or mutations where some succeed and some produce errors.

Improve `InputObject` creation by generating constructors

@Frozen666 requested this improvement as his graph.cool schema has frequent changes to input objects resulting in him having to change constructor functions that he hand-generates.

There probably needs to be support for passing in both required and optional arguments and combining them into one, so we discussed a design like this:

searchInput =
    SearchInput.fill { numResults = 50 } (\optionals -> { optionals | firstName = "John" })

And if there are no required args, it would just be

searchInput =
    SearchInput.fill (\optionals -> { optionals | firstName = "John" })

Type alias for input objects

I was using the generated input objects in my code e.g.

import Api.InputObject.UserAttrs exposing (UserAttrs)

As they were simple type aliases they were simple to use.

But now the lib generates:

type UserAttrs
    = UserAttrs { clientId : Int, name : String, timezone : String }

Meaning that if want to use this in my forms I need to unwrap and wrap UserAttrs all the time. I don't know why this type is needed, but ok.

So my possibilities atm are:

  • Create functions to map each input object
  • Duplicate the definition of the attrs

None is great. It would be nicer if the generator makes type aliases I can import e.g.

type alias UserAttrs
   = { clientId : Int, name : String, timezone : String }
   
type UserAttrsWrapped
    = UserAttrsWrapped UserAttrs

Does not work with example when generating from https://github.com/graphql/swapi-graphql

I'm runngin the server from https://github.com/graphql/swapi-graphql with npm start and generating with

npx graphqelm http://localhost:56196/? --output src --base Swapi

when running example from https://github.com/dillonkearns/graphqelm/blob/11.1.0/examples/src/Starwars.elm I got error

Starting downloads...

  ● Skinney/murmur3 2.0.6
  ● elm-community/json-extra 2.7.0
  ● elm-lang/virtual-dom 2.0.4
  ● elm-lang/core 5.1.1
  ● rtfeldman/elm-css-util 1.0.2
  ● krisajenkins/remotedata 4.4.0
  ● elm-community/list-extra 7.1.0
  ● lukewestby/elm-string-interpolate 1.0.2
  ● elm-lang/html 2.0.0
  ● elm-lang/websocket 1.0.2
  ● rtfeldman/hex 1.0.0
  ● dillonkearns/graphqelm 11.1.0
  ● elm-lang/http 1.0.0
  ● elm-community/string-extra 1.4.0
  ● rtfeldman/elm-css 
13.1.1
Packages configured successfully!
I cannot find module 'Swapi.Enum.Episode'.

Module 'Main' is trying to import it.

Potential problems could be:
  * Misspelled the module name
  * Need to add a source directory or new dependency to elm-package.json

Works with this https://graphqelm.herokuapp.com though.

`name` and `Name` fields generate ambiguous function names

@nicolasartman reported this compilation error. The cause appears to be that there is a field in the query named user and a field named User. These field names both normalize to user for their function names in the Query module.

For the User object (and only that one it seems), it generates two functions with the same name:

{-|

  - auth0UserId -
  - id -
-}
user : ({ auth0UserId : OptionalArgument String, id : OptionalArgument API.Scalar.Id } -> { auth0UserId : OptionalArgument String, id : OptionalArgument API.Scalar.Id }) -> SelectionSet decodesTo API.Object.User -> Field (Maybe decodesTo) RootQuery
user fillInOptionals object =
    let
        filledInOptionals =
            fillInOptionals { auth0UserId = Absent, id = Absent }

        optionalArgs =
            [ Argument.optional "auth0UserId" filledInOptionals.auth0UserId Encode.string, Argument.optional "id" filledInOptionals.id (\(API.Scalar.Id raw) -> Encode.string raw) ]
                |> List.filterMap identity
    in
        Object.selectionField "User" optionalArgs object (identity >> Decode.maybe)
user : SelectionSet decodesTo API.Object.User -> Field (Maybe decodesTo) RootQuery
user object =
    Object.selectionField "user" [] object (identity >> Decode.maybe)

This, as expected, fails compilation due to top-level identifier ambiguity.

@nicolasartman, did I diagnose the issue correctly here? Is it true that your API has both user and User fields under query?

This package has to make certain choices about how to normalize names so that they are valid in Elm. Like in this case, function names must start with a lowercase letter, so to normalize them we just change the first letter to be lowercase if it isn't.

Can you explain to me the purpose of your schema using both of these names? Is this a Graph.cool convention, or something that you or your team put in there? It's important for me to get a good sense of the use case to figure out what to do here. Thank you!

Hard to read module names

We have a mutation named BillingSubscriptions_UpdateBillingEmails
This is because our API is pretty big and we need to namespace things.

e.g. something like this in Ruby

module Mutation
   module BillingSubscriptions
      UpdateBillingEmails = Mutation....

We name "BillingSubscriptions_UpdateBillingEmails" for graphQL.

The payload in the introspection schema looks like:

           "type": {
              "kind": "OBJECT",
              "name": "BillingSubscriptions_UpdateBillingEmailsPayload",
              "ofType": null
            },

When graphqelm generates the Elm modules we end up with

Api.Object.BillingsubscriptionsUpdatebillingemailspayload

BillingsubscriptionsUpdatebillingemailspayload is a pretty hard name to parse mentally. It would be great if graphql would respect the camel case and do something else with the _. It don't suppose underscore are valid in a module name. Just BillingSubscriptionsUpdateBillingEmailsPayload would be better.

Allow enum types starting with underscore

GraphQL enum types, which start with an underscore are converted to elm modules that start with an underscore, which is not valid elm code.

APIs generated with graph.cool use an enum type, wich starts with an underscore. Here is the corresponding part of the introspection query:

{
          "inputFields": null,
          "name": "_ModelMutationType",
          "description": null,
          "interfaces": null,
          "enumValues": [
            {
              "name": "CREATED",
              "description": null,
              "isDeprecated": false,
              "deprecationReason": null
            },
            {
              "name": "UPDATED",
              "description": null,
              "isDeprecated": false,
              "deprecationReason": null
            },
            {
              "name": "DELETED",
              "description": null,
              "isDeprecated": false,
              "deprecationReason": null
            }
          ],
          "fields": null,
          "kind": "ENUM",
          "possibleTypes": null
        }

Subscriptions… where be there dragons?

The subscriptions docs for GraphQL warn about big 'ol breaking changes to the mechanisms they use, but don't say what might bite or be awful.

Warning The Subscriptions functionality in this package is in a highly experimental stage and may change rapidly or have issues that make it not ready for production code yet.

Would you mind elaborating on that, and why it wouldn't be ready? Is it an "I think it's OK but want people to test" or "this is more-or-less a huge experiment and really bad in odd ways so probably don't touch it until it stabilizes"?

Support Absinthe logger masking by allowing variables to be used instead of directly inserting input values

Because input values are inserted directly into the body of the query instead of being passed as variables, sensitive information can potentially be exposed in server logs.

This is the output I see in my request logs for an authenticate mutation on my API after switching to Graphqelm:

[info] POST /graphql
[debug] ABSINTHE schema=Api.Schema variables=%{}
---
mutation {
  authenticate(input: {email: "[email protected]", password: "password"}) {
    token
  }
}
---
[info] Sent 200 in 41ms

Absinthe, the server library I use, has a special logger to filter sensitive fields, but it only works if input is passed as variables.

Elm-format not working when installed as a package

Apologies Dillon - my elm-format patch is breaking prod at the moment.

The issue is that .bin doesn't get populated when graphqelm gets installed as a module.

Currently looking into a fix for this.

Indentation missing for `List.filterMap identity`

When optional args are present for an object, indentation of List.filterMap identity is missing. This indentation is necessary for the Elm compiler.

Below is an example of this issue from a generated Elm file for the Github API.

module Api.Object.Actor exposing (..)

import Graphqelm.Builder.Argument as Argument exposing (Argument)
import Graphqelm.FieldDecoder as FieldDecoder exposing (FieldDecoder)
import Graphqelm.Builder.Object as Object
import Graphqelm.SelectionSet exposing (SelectionSet)
import Api.Object
import Json.Decode as Decode
import Graphqelm.Encode as Encode exposing (Value)



selection : (a -> constructor) -> SelectionSet (a -> constructor) Api.Object.Actor
selection constructor =
    Object.object constructor
avatarUrl : ({ size : Maybe Int } -> { size : Maybe Int }) -> FieldDecoder String Api.Object.Actor
avatarUrl fillInOptionals =
    let
        filledInOptionals =
            fillInOptionals { size = Nothing }

        optionalArgs =
            [ Argument.optional "size" filledInOptionals.size (Encode.int) ]
|> List.filterMap identity -- 👈 Indentation should be present here
    in
      Object.fieldDecoder "avatarUrl" optionalArgs (Decode.string)


login : FieldDecoder String Api.Object.Actor
login =
      Object.fieldDecoder "login" [] (Decode.string)


resourcePath : FieldDecoder String Api.Object.Actor
resourcePath =
      Object.fieldDecoder "resourcePath" [] (Decode.string)


url : FieldDecoder String Api.Object.Actor
url =
      Object.fieldDecoder "url" [] (Decode.string)

Decoding fails for non-string scalars

In our application we have scalars such as Watts and WattHours where the back-end serializes the value to a JSON float. However, the Fields that Graphqelm generates expect all scalars to be serialized as strings:

power : Field (Maybe Api.Scalar.Watts) Api.Object.SomeRecord
power =
    Object.fieldDecoder "power" [] (Decode.string |> Decode.map Api.Scalar.Watts |> Decode.maybe)

This decoder causes the whole decode for this record to fail and return a Nothing

This is also problematic on the encoding side since the back-end will expect these values to be serialized as JSON floats there as well.

Incorrect Encoder Generation with Reserved keywords

It seems graphqelm will generate incorrect encoders if the record type contains reserved keywords like type.

Generated Code (Incorrect):

{-| Type for the Input input object.
-}
type alias Input =
    { apiVersion : String, type_ : String, payload : String }


{-| Encode a Input into a value that can be used as an argument.
-}
encodeInput : Input -> Value
encodeInput input =
    Encode.maybeObject
        [ ( "apiVersion", Encode.string input.apiVersion |> Just ), ( "type_", Encode.string input.type_ |> Just ), ( "payload", Encode.string input.payload |> Just ) ]

Schema:

input Input {
  apiVersion: String!
  type: String!
  payload: String!
}

type Mutation {
  processInput(input: Input!): JobEntity!
}

In the above example, graphqelm should generate encoder as follows:

{-| Encode a Input into a value that can be used as an argument.
-}
encodeInput : Input -> Value
encodeInput input =
    Encode.maybeObject
        [ ( "apiVersion", Encode.string input.apiVersion |> Just ), ( "type", Encode.string input.type_ |> Just ), ( "payload", Encode.string input.payload |> Just ) ]

Notice the lack of _ in the string type

Allow enum lower case values

Using lower case enum values results in type errors in the generated code due to generated lowercase type constructors in the elm code.

The GraphQL specification seems to allow lower case enum values, although most examples use uppercase values.
I am using graph.cool as my backend, which uses lowercase enum values in the schema by default.

Here is the corresponding part of the introspection query result:

{
          "inputFields": null,
          "name": "CategoryOrderBy",
          "description": null,
          "interfaces": null,
          "enumValues": [
            {
              "name": "id_ASC",
              "description": null,
              "isDeprecated": false,
              "deprecationReason": null
            },
            {
              "name": "id_DESC",
              "description": null,
              "isDeprecated": false,
              "deprecationReason": null
            },
            {
              "name": "name_ASC",
              "description": null,
              "isDeprecated": false,
              "deprecationReason": null
            },
            {
              "name": "name_DESC",
              "description": null,
              "isDeprecated": false,
              "deprecationReason": null
            }
          ],
          "fields": null,
          "kind": "ENUM",
          "possibleTypes": null
        }

Building operation from list stalls compiler

This seems like a compiler bug but I thought I'd report it here first, at least for reference purposes.

I was trying to do something like this to build a batch of mutations into a single request:

ids
    |> List.map (\id -> Api.Mutation.deleteFoo { id = Api.Scalar.Id id } SelectionSet.empty)
    |> List.foldl SelectionSet.with (Api.Mutation.selection identity)
    |> Graphqelm.Http.mutationRequest ""
    |> Graphqelm.Http.toTask

However this code puts compiler into an infinite loop (as far as I can tell).

Feel free to close if you think there's nothing to be done at the Graphqelm level and I'll open an issue on the compiler.

Subscribing with a token

I'm interested in the use-case of using token authentication on connect. That is, I login, obtain a token and add it to the socketUrl used to define the subscription:

ws://localhost:3000/socket/websocket?vsn=2.0.0&token=blah

If I define the subscription in the init I'm tied to a static url. See also https://hexdocs.pm/phoenix/channels.html#tying-it-all-together in the section Using Token Authentication. However I have not yet been able to make that work. I think part of the reason is the coupling between my model and Graphqelm.Subscription module, where in a few places, I am required to pass-in my full model.

Ideally, I would only pass you back the graphqlSubscriptionModel . subscriptions would then look like this:

subscriptions : Model -> Sub Msg
subscriptions model =
    listen GraphqlSubscriptionMsg model.graphqlSubscriptionModel

and the update:

GraphqlSubscriptionMsg graphqlSubscriptionMsg ->
    let
       ( updatedGraphqlSubscriptionModel, updateMsg ) =
            Graphqelm.Subscription.update graphqlSubscriptionMsg model.graphqlSubscriptionModel
    in
       ( { model | graphqlSubscriptionModel = updatedGraphqlSubscriptionModel }, updateMsg )

This means I have flexibility to do something such as (where CounterState is one of my data types):

type alias Model =
   {  graphqlSubscriptionModel : Maybe (Graphqelm.Subscription.Model Msg (Maybe CounterState)) }

Aside from the separation of concerns, I can then define a subscription dynamically. Interested in your thoughts. Let me know if I'm missing something really obvious! I can put up a pr if you like, but be sure to carefully review it :)

Feedback from my first spike

Hi @dillonkearns

I did a quick spike with graphqelm and I'm pleased at how productive I was in a short space of time. 😃

I love examples and it's great that there's something that I can refer to with Ellie.

module Main exposing (main)

import Graphqelm exposing (RootQuery)
import Graphqelm.Document as Document
import Graphqelm.Http
import Graphqelm.SelectionSet exposing (SelectionSet, with)
import Html exposing (Html, a, div, h1, h2, p, pre, text)
import Html.Attributes exposing (href, target)
import RemoteData exposing (RemoteData)
import Github.Object
import Github.Object.User as User
import Github.Query as Query


type alias Response =
    { user : Maybe User
    }


query : SelectionSet Response RootQuery
query =
    -- define the top-level query
    -- this syntax is based on the json decode pipeline pattern
    Query.selection Response
        |> with (Query.user { login = "justasitsounds" } user)


type alias User =
    -- as with JSON decoding, it's common to use type alias constructors
    { name : Maybe String
    , avatarUrl : String
    }


user : SelectionSet User Github.Object.User
user =
    User.selection User
        |> with User.name
        |> with (User.avatarUrl identity)


makeRequest : Cmd Msg
makeRequest =
    query
        |> Graphqelm.Http.buildQueryRequest
            "https://api.github.com/graphql"
        |> Graphqelm.Http.withHeader "authorization" "Bearer [secret!]"
        |> Graphqelm.Http.send (RemoteData.fromResult >> GotResponse)


type Msg
    = GotResponse Model


type alias Model =
    RemoteData Graphqelm.Http.Error Response


init : ( Model, Cmd Msg )
init =
    ( RemoteData.Loading
    , makeRequest
    )


view : Model -> Html.Html Msg
view model =
    div []
        [ div []
            [ h1 [] [ text "Generated Query" ]
            , pre [] [ text (Document.serializeQuery query) ]
            ]
        , div []
            [ h1 [] [ text "Response" ]
            , Html.text (toString model)
            ]
        ]


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GotResponse response ->
            ( response, Cmd.none )


main : Program Never Model Msg
main =
    Html.program
        { init = init
        , update = update
        , subscriptions = \_ -> Sub.none
        , view = view
        }

The only thing that wasn't obvious to me was what to do if you didn't want to enter any optional arguments. It might be worth mentioning in the docs that in this case, you can use identity. That might be obvious to seasoned Elm developers but it's always nice when documentation caters for programmers of all levels.

Good work on the http docs - I was able to figure out how to enter http headers easily. 👍

Unused imports in generated InputObject modules

Many of the imports in the InputObject modules generated from our API are unused which causes the Elm compiler to output warnings:

=================================== WARNINGS ===================================

-- unused import ---------------------- ./src/Api/InputObject/…Input.elm

Module `Api.Interface` is unused.

10| import Api.Interface
    ^^^^^^^^^^^^^^^^^^^^
Best to remove it. Don't save code quality for later!

-- unused import ---------------------- ./src/Api/InputObject/…Input.elm

Module `Api.Object` is unused.

11| import Api.Object
    ^^^^^^^^^^^^^^^^^
Best to remove it. Don't save code quality for later!

-- unused import ---------------------- ./src/Api/InputObject/…Input.elm

Module `Api.Union` is unused.

13| import Api.Union
    ^^^^^^^^^^^^^^^^
Best to remove it. Don't save code quality for later!

-- unused import ---------------------- ./src/Api/InputObject/…Input.elm

Module `Graphqelm.Field` is unused.

14| import Graphqelm.Field as Field exposing (Field)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Best to remove it. Don't save code quality for later!

-- unused import ---------------------- ./src/Api/InputObject/…Input.elm

Module `Graphqelm.Internal.Builder.Argument` is unused.

15| import Graphqelm.Internal.Builder.Argument as Argument exposing (Argument)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Best to remove it. Don't save code quality for later!

-- unused import ---------------------- ./src/Api/InputObject/…Input.elm

Module `Graphqelm.Internal.Builder.Object` is unused.

16| import Graphqelm.Internal.Builder.Object as Object
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Best to remove it. Don't save code quality for later!

-- unused import ---------------------- ./src/Api/InputObject/…Input.elm

Module `Graphqelm.SelectionSet` is unused.

19| import Graphqelm.SelectionSet exposing (SelectionSet)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Best to remove it. Don't save code quality for later!

-- unused import ---------------------- ./src/Api/InputObject/…Input.elm

Module `Json.Decode` is unused.

20| import Json.Decode as Decode
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Best to remove it. Don't save code quality for later!

Prettify error responses from graphql servers

Suppose a user installs graphqelm and runs the following:

$ graphqelm https://api.github.com/graphql

The above will get an error from the github graphql API server since the user did not submit an access token. Presently, the output for this case looks like this:

error { Error: GraphQL Error (Code: 401): {"response":{"message":"This endpoint requires you to be authenticated.","documentation_url":"https://developer.github.com/v3/#authentication","status":401},"request":{"query":"{\n    __schema {\n      queryType {\n        name\n      }\n      mutationType {\n        name\n      }\n      types {\n        ...FullType\n      }\n    }\n  }\n\n  fragment FullType on __Type {\n    kind\n    name\n    description\n    fields(includeDeprecated: true) {\n      name\n      description\n      args {\n        ...InputValue\n      }\n      type {\n        ...TypeRef\n      }\n      isDeprecated\n      deprecationReason\n    }\n    inputFields {\n      ...InputValue\n    }\n    interfaces {\n      ...TypeRef\n    }\n    enumValues(includeDeprecated: true) {\n      name\n      description\n      isDeprecated\n      deprecationReason\n    }\n    possibleTypes {\n      ...TypeRef\n    }\n  }\n\n  fragment InputValue on __InputValue {\n    name\n    description\n    type { ...TypeRef }\n    defaultValue\n  }\n\n  fragment TypeRef on __Type {\n    kind\n    name\n    ofType {\n      kind\n      name\n      ofType {\n        kind\n        name\n        ofType {\n          kind\n          name\n          ofType {\n            kind\n            name\n            ofType {\n              kind\n              name\n              ofType {\n                kind\n                name\n                ofType {\n                  kind\n                  name\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }"}}
    at GraphQLClient.<anonymous> (/Users/martimatix/.nvm/versions/node/v8.9.3/lib/node_modules/graphqelm/dist/bundle.js:12425:35)
    at step (/Users/martimatix/.nvm/versions/node/v8.9.3/lib/node_modules/graphqelm/dist/bundle.js:12378:23)
    at Object.next (/Users/martimatix/.nvm/versions/node/v8.9.3/lib/node_modules/graphqelm/dist/bundle.js:12359:53)
    at fulfilled (/Users/martimatix/.nvm/versions/node/v8.9.3/lib/node_modules/graphqelm/dist/bundle.js:12350:58)
    at <anonymous>
    at process._tickCallback (internal/process/next_tick.js:188:7)
  response:
   { message: 'This endpoint requires you to be authenticated.',
     documentation_url: 'https://developer.github.com/v3/#authentication',
     status: 401 },
  request:
   { query: '{\n    __schema {\n      queryType {\n        name\n      }\n      mutationType {\n        name\n      }\n      types {\n        ...FullType\n      }\n    }\n  }\n\n  fragment FullType on __Type {\n    kind\n    name\n    description\n    fields(includeDeprecated: true) {\n      name\n      description\n      args {\n        ...InputValue\n      }\n      type {\n        ...TypeRef\n      }\n      isDeprecated\n      deprecationReason\n    }\n    inputFields {\n      ...InputValue\n    }\n    interfaces {\n      ...TypeRef\n    }\n    enumValues(includeDeprecated: true) {\n      name\n      description\n      isDeprecated\n      deprecationReason\n    }\n    possibleTypes {\n      ...TypeRef\n    }\n  }\n\n  fragment InputValue on __InputValue {\n    name\n    description\n    type { ...TypeRef }\n    defaultValue\n  }\n\n  fragment TypeRef on __Type {\n    kind\n    name\n    ofType {\n      kind\n      name\n      ofType {\n        kind\n        name\n        ofType {\n          kind\n          name\n          ofType {\n            kind\n            name\n            ofType {\n              kind\n              name\n              ofType {\n                kind\n                name\n                ofType {\n                  kind\n                  name\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }',
     variables: undefined } }

Would it be possible to simply pretty print the response from the server?

Pagination API Brainstorm

I would like to collect use cases and ideas about pagination helpers in dillonkearns/graphqelm. I made a simple concrete example that does basic pagination using the Github API: examples/src/GithubPagination.elm.

Here is some reference material:

Facebook's Relay Connections Cursor Specification - this describes the standard format for pagination in GraphQL.
Post from Apollo blog on understanding connections - this explains it in more practical terms.

Apollo has a fetchMore function, but it may not be so relevant in the context of Elm because of the very different way of performing side-effects and using types:
https://www.apollographql.com/docs/react/recipes/pagination.html
Here's a blog post describing pagination with Apollo.

In my example code at the top of this post, it seems fairly straightforward without any helper functions and it's hard to imagine helper functions making it easier. I may be missing something or not accounting for some use cases, though. I'd love to hear other people's ideas on how pagination could be made simpler with helper functions or generated code.

The argument to function `send` is causing a mismatch: RemoteData.fromResult >> GotResponse

with "dillonkearns/graphqelm": "11.0.0 <= v < 11.0.1" or "dillonkearns/graphqelm": "11.1.0 <= v < 11.1.1"

and using code from https://ellie-app.com/55w73b8MDa1/0

Starting downloads...

  ● elm-community/string-extra 1.4.0
  ● elm-community/list-extra 7.1.0
  ● Skinney/murmur3 2.0.6
  ● dillonkearns/graphqelm 11.1.0
  ● lukewestby/elm-string-interpolate 1.0.2
  ● elm-lang/http 1.0.0
  ● elm-lang/core 5.1.1
  ● rtfeldman/elm-css-util 1.0.2
  ● krisajenkins/remotedata 4.3.3
  ● elm-lang/websocket 1.0.2
  ● elm-lang/html 2.0.0
  ● elm-community/json-extra 2.7.0
  ● elm-lang/virtual-dom 2.0.4
  ● rtfeldman/hex 1.0.0
  ● rtfeldman/elm-css 13.1.1

Packages configured successfully!
Detected errors in 1 module.


-- TYPE MISMATCH -------------------------------------------------- src/Main.elm

The argument to function `send` is causing a mismatch.

121|                      Http.send (RemoteData.fromResult >> GotResponse)
                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Function `send` is expecting the argument to be:

    Result (Graphqelm.Http.Error decodesTo) decodesTo -> msg

But it is:

    Result Graphqelm.Http.Error Response -> Msg

Map field decoder

In our legacy code we map decoders that might fail. If a decoder fails then the whole GraphQL response is consider invalid.

For example, we have something like this:

    type alias Thing
    	= {	createdAt : Time	}

The graphql response is decoded using:

    Decode.field "createtAt" Decoders.isoTime

Which uses:

    isoDate : Decoder Date
    isoDate =
        Decode.string
            &> \s ->
                case Times.dateFromIso s of
                    Err _ ->
                        Decode.fail <| "Invalid ISO date: " ++ s
    
                    Ok date ->
                        Decode.succeed date
    
    
    isoTime : Decoder Time
    isoTime =
        Decode.map Date.toTime isoDate

So if the time decoding fails the whole graphql response is invalid and we log an error.

This also helps to keep the possible failures at the edges.

I don't think we have a way to replicate this, do we?

Field.map maps the already decoded field which is already too late.


Also another use case, our Api generates Relay connections that are nullable, e.g.

users: null || { edges: null || { node: null || user } }

Unfortunatelly the library doesn't support removing these nulls.

So in our code we have something like:

       expectNonNull : Maybe a -> a
       expectNonNull maybeThing =
        case maybeThing of
            Just thing ->
                thing
    
            Nothing ->
                Debug.crash "Expected thing to be non-null"
       
       usersConn =
            Api.Object.UserConnection.selection identity
                |> with
                    (Api.Object.UserConnection.edges userEdges
                        |> Field.map expectNonNull
                        |> Field.map (List.map expectNonNull)
                    )
    
        userEdges =
            Api.Object.UserEdge.selection identity
                |> with
                    (Api.Object.UserEdge.node userSelection
                        |> Field.map expectNonNull

This works, but what we would really want is to fail the whole decoding process if we found a null.


Any ideas? Could it be possible to pass an optional decoder to map a field?
Thanks

`Api.Object` does not expose `Query`

In the Github API, under Query, there is a field called Relay that gives back a type of Query.

In the generated Elm code, this is defined as:

relay : SelectionSet relay Api.Object.Query -> FieldDecoder relay RootQuery
relay object =
    Object.single "relay" [] (object)

However, Api.Object.Query is not defined and therefore, the Elm code cannot be compiled.

Filter from Graphcool API causes self-referential type & thus compilation error

First, thank you for making such an awesome library & tool! I'm really excited to start using this on a side project of mine that uses https://www.graph.cool's hosted API system for the backend, but there's one small snag I noticed.

When I generate the code for my API via graphqelm https://api.graph.cool/simple/v1/cj1rh31pc8ykf0127fwssmw32 --base API, some of the generated filters seem to reference themselves and this causes compilation to fail. For example, in UserFilter.elm, it generates:

type alias UserFilter =
    { and : OptionalArgument (List API.InputObject.UserFilter.UserFilter), or : OptionalArgument (List API.InputObject.UserFilter.UserFilter), .............

which I think references that type alias itself in its definition. The exact error message is "Cannot find type API.InputObject.UserFilter.UserFilter.

If there's anything more I can do to help replicate this issue or look into it further, please let me know and I'll be more than happy to do so!

Add a map fn for Graphqelm.Http.Error

Version 11 made Graphqelm.Http.Error to be different types. So something like this is not possible anymore:

type Msg 
     = MonkeyFetched (Result Grapheqlm.Http.Error  Monkey)
     | MonkeyCreated (Result Grapheqlm.Http.Error CreateMonkeyMutationResponse)

case msg of

    MonkeyFetched result ->
        case result of
            Err err ->
                ({model | monkey = Failure err})    
            ....

    MonkeyCreated result ->
        case result of
            Err err ->
                ({model | monkey = Failure err})    

In the above case it make sense to treat the fetch and creation in a similar way. Having the error being different types prevents this from compiling.

The error type needs to be homogenised first e.g.

mapGraphelmError : (a -> b) -> Graphqelm.Http.Error a -> Graphqelm.Http.Error b
mapGraphelmError fn error =
    case error of 
         Graphqelm.Http.HttpError e ->
            Graphqelm.Http.HttpError e

         Graphqelm.Http.GraphqlError parsedData errors ->
            case parsedData of
                Graphqelm.Http.GraphqlError.ParsedData parsed ->
                     Graphqelm.Http.GraphqlError (Graphqelm.Http.GraphqlError.ParsedData (fn parsed)) errors
                
                Graphqelm.Http.GraphqlError.UnparsedData v ->
                     Graphqelm.Http.GraphqlError (Graphqelm.Http.GraphqlError.UnparsedData v) errors

And then

type Msg 
     = MonkeyFetched (Result (Grapheqlm.Http.Error (Maybe Monkey)) (Maybe Monkey))
     | MonkeyCreated (Result (Grapheqlm.Http.Error CreateMonkeyMutationResponse) CreateMonkeyMutationResponse)

MonkeyCreated result ->
            case result of
                Err mutationErr ->
                    let
                        err =
                            mapGraphelmError
                                (always Nothing)
                                mutationErr
                    in
                        ( { model | data = RemoteData.Failure err }
                        , ...
                        )

So, consider adding a mapError function unless there is better solution.

Expose Error

If I get an Error (Graphqelm.Http.Error) I cannot do anything with it. I need to pattern match on the constructors in order to log the error. e.g.

logGraphQLError : Graphqelm.Http.Error -> Cmd msg
logGraphQLError error =
    case error of
        Graphqelm.Http.HttpError e ->
            logHttpError e

        Graphqelm.Http.GraphqlError errors ->
            ...

But the constructors are not exposed. Please expose the constructors i.e. exposing Error(..)

Field name clash with Elm reserved words

I have a simple schema with this object definition

type Service {
  name : String
  port :  Int
}

This causes graphqelm to throw an error, which actually seems to come from elm-format. The error message is formated weird so I won't paste it here.

Looking at the generated code (and the elm-format error) it was clear that port can not be used as a field name. I expect the same will be true for other words like module and import.

This is not a big problem, so this issue is more to see if it was possible to catch this naming clash with relevant reserved words in Elm and display a better error message.

Clarify on imports in the Gitbook

I was a little bit lost when reading the Gitbook since I wondered where the with function came from.

Of course I had just skimmed over the first article and hence was confused where we imported with. I think the main source of my confusion stemmed from the fact that the identity function was described in detail but nothing could be found for with on the linked page.

Clearly one can look it up in the (awesome) docs but I think this could be a bit hindering for newcommers.

Anyways, not a high priority. Just wanted you to know 😃
Thanks for the awesome library! 👍

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.