morpheus-graphql: Morpheus GraphQL

This is a package candidate release! Here you can preview how this package release will appear once published to the main package index (which can be accomplished via the 'maintain' link below). Please note that once a package has been published to the main package index it cannot be undone! Please consult the package uploading documentation for more information.

[maintain] [Publish]

Build GraphQL APIs with your favourite functional language!


[Skip to Readme]

Properties

Versions 0.0.1, 0.1.0, 0.1.1, 0.2.0, 0.2.1, 0.2.2, 0.3.0, 0.3.1, 0.4.0, 0.4.0, 0.5.0, 0.6.0, 0.6.1, 0.6.2, 0.7.0, 0.7.1, 0.8.0, 0.9.0, 0.9.1, 0.10.0, 0.11.0, 0.12.0, 0.13.0, 0.14.0, 0.14.1, 0.15.0, 0.15.1, 0.16.0, 0.17.0, 0.18.0, 0.19.0, 0.19.1, 0.19.2, 0.19.3, 0.20.0, 0.20.1, 0.21.0, 0.22.0, 0.22.1, 0.23.0, 0.24.0, 0.24.1, 0.24.2, 0.24.3, 0.25.0, 0.26.0, 0.27.0, 0.27.1, 0.27.2, 0.27.3, 0.28.0, 0.28.1
Change log changelog.md
Dependencies aeson (>=1.4.4.0 && <=1.6), base (>=4.7 && <5), bytestring (>=0.10.4 && <0.11), containers (>=0.4.2.1 && <0.7), filepath (>=1.1 && <1.5), megaparsec (>=7.0.0 && <8.0), morpheus-graphql, mtl (>=2.0 && <=2.2.2), optparse-applicative (>=0.12 && <0.15), scientific (>=0.3.6.2 && <0.4), scotty, template-haskell, text (>=1.2.3.0 && <1.3), transformers (>=0.3.0.0 && <0.6), unordered-containers (>=0.2.8.0 && <0.3), uuid (>=1.0 && <=1.4), vector (>=0.12.0.1 && <0.13), wai, wai-websockets (>=1.0 && <=3.5), warp, websockets (>=0.11.0 && <=0.12.5.3) [details]
License BSD-3-Clause
Copyright (c) 2019 Daviti Nalchevanidze
Author Daviti Nalchevanidze
Maintainer d.nalchevanidze@gmail.com
Category web, graphql
Home page https://morpheusgraphql.com
Bug tracker https://github.com/nalchevanidze/morpheus-graphql/issues
Source repo head: git clone https://github.com/nalchevanidze/morpheus-graphql
Uploaded by nalchevanidze at 2019-10-08T23:30:39Z

Modules

[Index] [Quick Jump]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees


Readme for morpheus-graphql-0.4.0

[back to package description]

Morpheus GraphQL Hackage CircleCI

Build GraphQL APIs with your favourite functional language!

Morpheus GraphQL (Server & Client) helps you to build GraphQL APIs in Haskell with native haskell types. Morpheus will convert your haskell types to a GraphQL schema and all your resolvers are just native Haskell functions. Mopheus GraphQL can also convert your GraphQL Schema or Query to Haskell types and validate them in compile time.

Morpheus is still in an early stage of development, so any feedback is more than welcome, and we appreciate any contribution! Just open an issue here on GitHub, or join our Slack channel to get in touch.

Getting Started

Setup

To get started with Morpheus, you first need to add it to your project's dependencies, as follows (assuming you're using hpack):

package.yml

dependencies:
  - morpheus-graphql

Additionally, you should tell stack which version to pick:

stack.yml

resolver: lts-13.30

extra-deps:
  - morpheus-graphql-0.4.0
  - aeson-1.4.4.0
  - time-compat-1.9.2.2

As Morpheus is quite new, make sure stack can find morpheus-graphql by running stack update

Building your first GraphQL API

with GraphQL syntax

schema.gql

type Query {
  deity(name: String!): Deity!
}

type Deity {
  name: String!
  power: String
}

API.hs

{-# LANGUAGE DeriveGeneric         #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns        #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TemplateHaskell       #-}
{-# LANGUAGE TypeFamilies          #-}

module API (api) where

import qualified Data.ByteString.Lazy.Char8 as B

import           Data.Morpheus              (interpreter)
import           Data.Morpheus.Document     (importGQLDocumentWithNamespace)
import           Data.Morpheus.Types        (GQLRootResolver (..), IORes)
import           Data.Text                  (Text)

importGQLDocumentWithNamespace "schema.gql"

rootResolver :: GQLRootResolver IO () () (Query IORes) () ()
rootResolver =
  GQLRootResolver
    {queryResolver = return Query {queryDeity}, mutationResolver = pure (), subscriptionResolver = pure ()}
  where
    queryDeity QueryDeityArgs {queryDeityArgsName} = pure Deity {deityName, deityPower}
      where
        deityName _ = pure "Morpheus"
        deityPower _ = pure (Just "Shapeshifting")

api :: ByteString -> IO ByteString
api = interpreter rootResolver

Template Haskell Generates types: Query , Deity, DeityArgs, that can be used by rootResolver

importGQLDocumentWithNamespace will generate Types with namespaced fields. if you don't need napespacing use importGQLDocument

with Native Haskell Types

To define a GraphQL API with Morpheus we start by defining the API Schema as a native Haskell data type, which derives the Generic typeclass. Lazily resolvable fields on this Query type are defined via a -> IORes b, representing resolving a set of arguments a to a concrete value b.

data Query = Query
  { deity :: DeityArgs -> IORes Deity
  } deriving (Generic, GQLType)

data Deity = Deity
  { fullName :: Text         -- Non-Nullable Field
  , power    :: Maybe Text   -- Nullable Field
  } deriving (Generic)

instance GQLType Deity where
  type  KIND Deity = OBJECT

data DeityArgs = DeityArgs
  { name      :: Text        -- Required Argument
  , mythology :: Maybe Text  -- Optional Argument
  } deriving (Generic)

For each field in the Query type defined via a -> IORes b (like deity) we will define a resolver implementation that provides the values during runtime by referring to some data source, e.g. a database or another API. Fields that are defined without a -> IORes b you can just provide a value.

In above example, the field of DeityArgs could also be named using reserved identities (such as: type, where, etc), in order to avoid conflict, a prime symbol (') must be attached. For example, you can have:

data DeityArgs = DeityArgs
  { name      :: Text        -- Required Argument
  , mythology :: Maybe Text  -- Optional Argument
  , type'     :: Text
  } deriving (Generic)

The field name in the final request will be type instead of type'. The Morpheus request parser converts each of the reserved identities in Haskell 2010 to their corresponding names internally. This also applies to selections.

resolveDeity :: DeityArgs -> IORes Deity
resolveDeity args = gqlResolver $ askDB (name args) (mythology args)

askDB :: Text -> Maybe Text -> IO (Either String Deity)
askDB = ...

Note that the type a -> IORes b is just Synonym for a -> ExceptT String IO b

To make this Query type available as an API, we define a GQLRootResolver and feed it to the Morpheus interpreter. A GQLRootResolver consists of query, mutation and subscription definitions, while we omit the latter for this example:

rootResolver :: GQLRootResolver IO () () Query () ()
rootResolver =
  GQLRootResolver
    { queryResolver = return Query {deity = resolveDeity}
    , mutationResolver = return ()
    , subscriptionResolver = return ()
    }

gqlApi :: ByteString -> IO ByteString
gqlApi = interpreter rootResolver

As you can see, the API is defined as ByteString -> IO ByteString which we can either invoke directly or use inside an arbitrary web framework such as scotty or serverless-haskell. We'll go for scotty in this example:

main :: IO ()
main = scotty 3000 $ post "/api" $ raw =<< (liftIO . gqlApi =<< body)

If we now send a POST request to http://localhost:3000/api with a GraphQL Query as body for example in a tool like Insomnia:

query GetDeity {
  deity (name: "Morpheus") {
    fullName
    power
  }
}

our query will be resolved!

{
  "data": {
    "deity": {
      "fullName": "Morpheus",
      "power": "Shapeshifting"
    }
  }
}

Serverless Example

If you are interested in creating a Morpheus GraphQL API with Serverless, you should take a look at our example in this repository: Mythology API it is our example project build with Morpheus GraphQL and Serverless-Haskell, where you can query different mythology characters with GraphiQL.

Mythology API is deployed on : api.morpheusgraphql.com where you can test it with GraphiQL

Mythology Api

Advanced topics

Enums

You can use Union Types as Enums, but they're not allowed to have any parameters.

data City
  = Athens
  | Sparta
  | Corinth
  | Delphi
  | Argos
  deriving (Generic)

instance GQLType City where
  type KIND City = ENUM

Union types

To use union type, all you have to do is derive the GQLType class. Using GraphQL fragments, the arguments of each data constructor can be accessed from the GraphQL client.

data Character
  = DEITY Deity
  | HUMAN Human
  deriving (Generic)

instance GQLType Character where
  type KIND City = UNION

Scalar types

To use custom scalar types, you need to provide implementations for parseValue and serialize respectively.

data Odd = Odd Int  deriving (Generic)

instance GQLScalar Odd where
  parseValue (Int x) = pure $ Odd (...  )
  parseValue (String x) = pure $ Odd (...  )
  serialize  (Odd value) = Int value

instance GQLType Odd where
  type KIND Odd = SCALAR

Applicative and Monad instance

The Resolver type has Applicative and Monad instances that can be used to compose resolvers.

Introspection

Morpheus converts your schema to a GraphQL introspection automatically. You can use tools like Insomnia to take a look at the introspection and validate your schema. If you need a description for your GQLType inside of the introspection you can define the GQLType instance manually and provide an implementation for the description function:

data Deity = Deity
{ ...
} deriving (Generic)

instance GQLType Deity where
  description = const "A supernatural being considered divine and sacred"

screenshots from Insomnia

alt text alt text alt text

Mutations

In addition to queries, Morpheus also supports mutations. The behave just like regular queries and are defined similarly: Just exchange deriving GQLQuery for GQLMutation and declare them separately at the GQLRootResolver definition

newtype Mutation = Mutation
  { createDeity :: Form -> IOMutRes Deity
  } deriving (Generic, GQLType)

createDeityMutation :: Form -> IOMutRes Deity
createDeityMutation = ...

rootResolver :: GQLRootResolver IO Query Mutation ()
rootResolver =
  GQLRootResolver
    { queryResolver = return Query {...}
    , mutationResolver = return Mutation {
       createDeity = createDeityMutation
    }
    , subscriptionResolver = return ()
    }

gqlApi :: ByteString -> IO ByteString
gqlApi = interpreter rootResolver

Subscriptions

im morpheus subscription and mutation communicating with Events, Event consists with user defined Channel and Content.

every subscription has own Channel by which will be triggered


data Channel
  = ChannelA
  | ChannelB

data Content
  = ContentA Int
  | ContentB Text

newtype Query = Query
  { deity :: () -> IORes Deity
  } deriving (Generic, GQLType)

newtype Mutation = Mutation
  { createDeity :: () -> IOMutRes Channel Content Deity
  } deriving (Generic, GQLType)

newtype Subscription = Subscription
  { newDeity :: () -> IOSubRes Channel Content Deity
  } deriving (Generic, GQLType)

rootResolver :: GQLRootResolver IO Channel Content Query Mutation Subscription
rootResolver =
  GQLRootResolver
    { queryResolver = return Query {deity = const fetchDeity}
    , mutationResolver = return Mutation {createDeity}
    , subscriptionResolver = return Subscription {newDeity}
    }
  where
    fetchDeity = resolver $ dbDeity "" Nothing
    createDeity _args = toMutResolver [Event {channels = [ChannelA], content = ContentA 1}] fetchDeity
    newDeity _args = SubResolver {subChannels = [ChannelA], subResolver}
      where
        subResolver (Event [ChannelA] (ContentA _value)) = resolver $ dbDeity "" Nothing -- resolve New State
        subResolver (Event [ChannelA] (ContentB value))  = resolver $ dbDeity value Nothing -- resolve New State
        subResolver _                                    = fetchDeity -- Resolve Old State

Morpheus GraphQL Client with Template haskell QuasiQuotes

defineByDocumentFile
    "./schema.gql"
  [gql|
    query GetHero ($character: Character)
      {
        deity (fatherOf:$character) {
          name
          power
          worships {
            deity2Name: name
          }
        }
      }
  |]

with schema:

input Character {
  name: String!
}

type Deity {
  name: String!
  worships: Deity
}

will validate query and Generate:

data GetHero = GetHero {
  deity: DeityDeity
}

-- from: {user
data DeityDeity = DeityDeity {
  name: Text,
  worships: Maybe DeityWorshipsDeity
}

-- from: {deity{worships
data DeityWorshipsDeity = DeityWorshipsDeity {
  name: Text,
}

data GetHeroArgs = GetHeroArgs {
  getHeroArgsCharacter: Character
}

data Character = Character {
  characterName: Person
}

as you see, response type field name collision can be handled with GraphQL alias.

with fetch you can fetch well typed response GetHero.

  fetchHero :: Args GetHero -> m (Either String GetHero)
  fetchHero = fetch jsonRes args
      where
        args = GetHeroArgs {getHeroArgsCharacter = Person {characterName = "Zeus"}}
        jsonRes :: ByteString -> m ByteString
        jsonRes = <GraphQL APi>

types can be generatet from introspection too:

defineByIntrospectionFile "./introspection.json"

Morpheus CLI for Code Generating

Generating dummy Morpheus Api from schema.gql

morpheus build src/schem.gql src/GQLApi.hs

this command will generate Haskell API and resolvers, resolvers will resolve default values for every object

About

The name

Morpheus is the greek god of sleep and dreams whose name comes from the greek word μορφή meaning form or shape. He is said to be able to mimic different forms and GraphQL is good at doing exactly that: Transforming data in the shape of many different APIs.

Team

Morpheus is written and maintained by nalchevanidze

Roadmap