GithubHelp home page GithubHelp logo

morpheusgraphql / morpheus-graphql Goto Github PK

View Code? Open in Web Editor NEW
406.0 10.0 63.0 11.81 MB

Haskell GraphQL Api, Client and Tools

Home Page: https://morpheusgraphql.com

License: MIT License

Haskell 71.23% Makefile 0.03% HTML 28.64% Dockerfile 0.01% TypeScript 0.09%
haskell graphql web graphql-api graphql-server graphql-subscriptions graphql-haskell haskell-graphql graphql-introspection graphql-client

morpheus-graphql's Introduction

Morpheus GraphQL Hackage CI

Build GraphQL APIs with your favorite 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. Morpheus 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.

Please note that this readme file provides only a brief introduction to the library. If you are interested in more advanced topics, visit Docs.

Since version v0.28.0, Morpheus GraphQL implements the [https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md](graphql-ws GraphQL over WebSocket Protocol), and no longer uses the https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md protocol.

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
  - morpheus-graphql-core
  - morpheus-graphql-subscriptions

Additionally, you should tell stack which version to pick:

stack.yml

resolver: lts-16.2

extra-deps:
- morpheus-graphql-0.28.0
- morpheus-graphql-core-0.28.0
- morpheus-graphql-app-0.28.0
- morpheus-graphql-code-gen-0.28.0
- morpheus-graphql-code-gen-utils-0.28.0
- morpheus-graphql-server-0.28.0
- morpheus-graphql-client-0.28.0
- morpheus-graphql-subscriptions-0.28.0

Building your first GraphQL API

with GraphQL syntax

schema.gql

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

"""
Description for Deity
"""
type Deity {
  """
  Description for name
  """
  name: String!
  power: String @deprecated(reason: "some reason for")
}

API.hs

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

module API (api) where

import Data.ByteString.Lazy.Char8 (ByteString)
import Data.Morpheus (interpreter)
import Data.Morpheus.Document (importGQLDocument)
import Data.Morpheus.Types (RootResolver (..), Undefined (..))
import Data.Text (Text)

importGQLDocument "schema.gql"

rootResolver :: RootResolver IO () Query Undefined Undefined
rootResolver =
  RootResolver
    { queryResolver = Query {deity},
      mutationResolver = Undefined,
      subscriptionResolver = Undefined
    }
  where
    deity DeityArgs {name} =
      pure
        Deity
          { name = pure name,
            power = pure (Just "Shapeshifting")
          }

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

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

descriptions and deprecations will be displayed in introspection.

importGQLDocumentWithNamespace will generate Types with namespaced fields. If you don't need namespace 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 type class. Using the DeriveAnyClass language extension we then also derive instances for the GQLType type class. Lazily resolvable fields on this Query type are defined via a -> ResolverQ () IO b, representing resolving a set of arguments a to a concrete value b.

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

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

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

For each field in the Query type defined via a -> m 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 -> m 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, GQLType)

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 -> ResolverQ () IO Deity
resolveDeity DeityArgs { name, mythology } = liftEither $ dbDeity name mythology

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

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

rootResolver :: RootResolver IO () Query Undefined Undefined
rootResolver =
  RootResolver
    { queryResolver = Query {deity = resolveDeity}
    , mutationResolver = Undefined
    , subscriptionResolver = Undefined
    }

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

Showcase

Below are the list of projects using Morpheus GraphQL. If you want to start using Morpheus GraphQL, they are good templates to begin with.

Edit this section and send PR if you want to share your project.

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

  • Medium future:
    • Stabilize API
    • Specification-isomorphic error handling
  • Long term:
    • Support all possible GQL features
    • Performance optimization

morpheus-graphql's People

Contributors

414owen avatar alistairb avatar cmdea avatar dandoh avatar dependabot[bot] avatar gelisam avatar github-actions[bot] avatar horus avatar hovind avatar krisajenkins avatar lorenzo avatar msvbg avatar nalchevanidze avatar nerdgguy avatar notquiteamonad avatar ocharles avatar ollef avatar piq9117 avatar pranaysashank avatar psilospore avatar ropwarejb avatar russellmcc avatar ryota-ka avatar sebashack avatar smatting avatar sordina avatar theobat avatar thomasjm avatar tristancacqueray avatar vmchale 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

morpheus-graphql's Issues

Feature/ type familiy KIND with type class GQLType

type instance KIND Deity = OBJECT

instance GQLType Deity where
  description _ = "Custom Description for Client Defined User Type"

data Deity = Deity
  { fullName :: Text
  } deriving (Generic)

will be changed to

instance GQLType Deity where
  type KIND Deity = OBJECT
  description _ = "Custom Description for Client Defined User Type"

data Deity = Deity
  { fullName :: Text
  } deriving (Generic)

different behaviour acourding to Function

data Coordinates = Coordinates
  { latitude  :: ScalarOf Modulo7
  , longitude :: Int
  } deriving (Show, Generic, Data)

instance GQLKind Coordinates where
  description _ = "just random latitude and longitude"
  kind _ = INPUT_OBJECT


class Kind (GQL con a) where 
    Introspect :: GQL con a = > proxy a -> TypeLib
    encode :: GQL con a => a -> Sel -> JSType

type SELECTION a = GQL Selection a


Instance Kind SELECTION User 

... deriving (Kind SELECTION)

data family GQL con a :: Constraint
Instance GQL Selection a = Selectors (Rep a)  (Text,Field)
Instance GQL Enum a = GEnum a

Support/Tuple,Map,Set,

support Tuple

every (K,V) automatically generates GraphQL types Pair_K_V

type Pair_K_V {
  key: K
  value: V
}

support Map

every Map K V automatically generates GraphQL types Pair_K_V and Map_K_V

type Pair_K_V {
  key: K
  value: V
}

type MapKind_K_V {
  __typeName: String!
  size: Int!
  keys: [K!]!,
  values(elem:[K]): [V!]!,
  pairs(elem:[K]): [Pair_K_V!]!
}

so

{  
  person{
    size
    pairs(elem:["robin","carl"]) {
       age
       hobby
    }
  }
}

will resolve

{
  "person": {
    "size": 200,
    "pairs": [{
        "key": "robin",
        "value": {
          "age": 32,
          "hobby": "Joging"
        }
      },
      {
        "key": "carl",
        "value": {
          "age": 25,
          "hobby": "Jazz music"
        }
      }
    ]
  }
}

support Set

every Set V automatically converted to GraphQL [V]

Refactor: move Morphus to root directory

at the moment the library is located in the ./morphus folder, that produces redundant duplicates of Readme changelog and ...

it will be feel more natural to have it at the root level

Feature/Enhance Server with custom InputUnion Validation

support Input Union

Enhance GraphQL server so that it recognises input object, but does not break client Libraries

data Cat = Cat { catName :: Text }  

data Dog = Dog { dogName :: Text } 

type instance Kind Pet = INPUT_UNION

data Pet = CAT Cat | DOG Dog deriving (Generic, GQLType) 

or Just

type instance Kind Pet = INPUT_UNION

data Pet = Cat { catName :: Text }  | Dog { dogName :: Text } deriving (Generic, GQLType)

union types like Pet , can be automatically derived as GraphQL enum for possible type names and input that contains actual types as fields

enum PetTags { 
  Cat
  Dog
}

input Pet {
    typeTag: PetTags!
    typeCat: Cat
    typeDog: Dog
}

so server decodes input:

{ 
  "__typename": "Dog",
  "Dog" : {
     "dogName" : "Milo"
   }
} 

to

Dog { dogName = "Milo" }

and input:

{ 
  "typeTag": "Cat",
  "typeDog" : {
     "dogName" : "Milo"
   }
} 

will throw Validation Error

so we don't break client libraries because we use Standart GraphQL types and validate them differently in Server. and we don't modify GraphQL Schema

Other communication than just issues

It would be nice if there was some kind of channel like Spectrum/Discord/Slack/IRC where you can discuss/ask questions without opening an issue.

Bug/No empty Operator

when Subscription or Mutation is not defined GQLSchema generates empty object for it, that breaks autocorection on GraphiQL

Generate 'Schema.hs' Haskell GraphQL Type Definitions and Resolvers from 'Schema.gql'

Morpheus CLI that converts

schema.gql

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

type Deity {
  fullName: String!
  power : String
} 

to haskell API with default Values:

Api.hs

data Query = Query
{ deity :: DeityArgs -> ResM Deity
} deriving (Generic)

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

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

Deity.hs

data Deity = Deity
{ fullName :: Text
, power :: Maybe Text
} deriving (Generic)

instance GQLType Deity where
    type KIND Deity = OBJECT

data DeityArgs = DeityArgs
{ name :: Text
, mythology :: Maybe Text
} deriving (Generic)


resolveDeity :: DeityArgs -> ResM Deity
resolveDeity _ = return Deity {
    fullName = "",
    power = Nothing
}

so can be easily Migrated GraphQL api from node.js or any other Language

SyntaxValidation/ add error Messages and positions information

on a syntax error attoparsec shows only :

{
  "errors": [
    {
      "message": "Syntax Error: \"endOfInput\"",
      "locations": [
        {
          "line": 1,
          "column": -1
        }
      ]
    }
  ]
}

or

{
  "errors": [
    {
      "message": "Syntax Error: \"string\"",
      "locations": [
        {
          "line": 1,
          "column": -1
        }
      ]
    }
  ]
}

it would be nice to have a description of the failure and position.

  • basic error message : expected "a" found "b"
  • position Information

Validate/Fragments: full validation plus tests

full validation of fragments + tests:

done:

  • unknownTargetType: ✅ + 🧪
  • cannotSpreadWithinItself: ✅ + 🧪

todo:

  • unusedFragment
    if fragment is defined but not used in query Interpreter should throw error, can be validated with same way as Variable
 { 
   "message":"Fragment \"<Name>\" is never used." ,
   "position": ...
 }
  • nameColision:

    fragment F on User {
      email
    }
    fragment F on User {
      email
    }

    should throw error:

    {
          "message": "There can be only one fragment named \"F\".",
          "locations": [
            {
              "line": 7,
              "column": 10
            },
            {
              "line": 11,
              "column": 10
            }
          ]
    }

GQL Subscription Apollo subscriptions-transport-ws

parse GQL requests according apollo Sec-WebSocket-Protocol:

  • old vesrion: graphql-subscriptions
      {
      "id": 0,
      "type":"subscription_start",
      "payload": {},
      "query": "subscription Simp...."
     }
  • new version: graphql-ws
    {
      "id":"0",
      "type":"start",
      "payload": { "query":"subscription Simp...."}
     }

Support/ Union types

#112

type instance KIND Character = UNION

data Character = DEITY Deity | TITAN Titan deriving (Generic, GQLType)

Tests for UnionType:
#127

Proposal/ Should Operators derived as `type family KIND instances`

now if you want define Query Mutation Or Subscription, you should write

data Query = Query { ...  } deriving (Generic, GQLQuery)

data Mutation = Mutation { ...} deriving (Generic, GQLMutation)

data Subscription = Subscription {...} deriving (Generic, GQLSubscription)

one alternative is to derive it as all other Kinds ?

type instance KIND Query = QUERY

data Query = Query { ...  } deriving (Generic, GQLType)


type instance KIND Mutation = MUTATION

data Mutation = Mutation { ...} deriving (Generic, GQLType)


type instance KIND Subscription = SUBSCRIPTION

data Subscription = Subscription {...} deriving (Generic, GQLType)

Split up example and libary

We need to structure the source files and cabal / hpack package in a way that the project can be used as a library and published to hackage.
Split the example off the library code, into a separate directory and adjust the package.yml accordingly

Refactor: improve Readability

  • better directory structure
  • more standards
  • more comments about what each part of code does
  • add readme and docs for contributor

Support: Inline Fragments

   ... on Deity {
      power
  }

Parser should recognize inline fragments.

  • add feature to parser
  • validate parsed value
  • add tests
  • update specifications.md and changelog.md

Desctiptions: type descriptions as Instance Method

-- without decription 
data A = { a:: Int ,... } deriving (Show,Data,Generic,GQLSelection)

-- with decription 
data A = { a:: Int ,... } deriving (Show,Data,Generic)

instance GQLSelection A where
    description  = "Odd custom scalar type"

Spec compliance: Int vs Int32

This may be a little bit picky, but the spec says that Ints are 32-bit integers. Haskell's Int type, used here, is at least a signed 30-bit integer, probably due to tagged pointer representation. (Although on my system it's a 64-bit int, and I suppose this is quite common).

For full spec compliance, the API could be changed to use Int32 from Data.Int everywhere. This might make things a little bit uglier because you would have to use Int32 everywhere, and I'm not sure about GHC implementation details but maybe Int32 is boxed on 32-bit platforms due to not being able to use the 30-bit optimization trick. Or it could be argued that this is not worth the trouble.

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.