Coder Social home page Coder Social logo

jordwalke / graphql_ppx Goto Github PK

View Code? Open in Web Editor NEW

This project forked from mhallin/graphql_ppx

2.0 3.0 1.0 4.17 MB

GraphQL PPX rewriter for Bucklescript/ReasonML

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

Makefile 0.39% Shell 1.15% JavaScript 3.04% OCaml 94.24% C++ 1.18%

graphql_ppx's Introduction

GraphQL syntax extension for Bucklescript/ReasonML

Work in progress: only a small subset of GraphQL is implemented and there are probably a lot of bugs.

Build Status Build Status npm

This library lets you construct type-safe and validated queries at compile time, and generates response validation code for you. If you're writing a Bucklescript app that talks to a GraphQL server, this library will cut down on the boilerplate you have to write.

It is compatible with both OCaml and ReasonML syntax. There are no runtime dependencies except for Js.Json and Js.Dict, both included in the Bucklescript standard library.

Installation

Assuming that you've already got a Bucklescript project set up, installing this syntax extension consists of two steps:

First, add this package as a dependency to your package.json:

yarn add --dev graphql_ppx
# or, if you use npm:
npm install --saveDev graphql_ppx

Second, add the PPX to your bsconfig.json:

{
    "ppx-flags": [
        "graphql_ppx/ppx"
    ]
}

Note: If you want to use this, make sure to read the limitations at the bottom of this readme first!

Upgrading from older versions

The OPAM package graphql_ppx is no longer needed. If you've used an earlier version of this PPX you can safely remove this package.

Examples

If you add a field that does not exist, you'll get a compiler error on the exact location this happens. This automatically works with Merlin, giving you immediate feedback in your editor:

Misspelled field, immediate compiler errors

Variables sent to queries and mutations are of course typed too. Nullable variables are translated to optional labelled arguments, while non-null variables become mandatory arguments:

Remove a variable argument, get a compiler error

(The Api.sendQuery function here is a small wrapper around bs-fetch, check it out below)

The result of a query is turned into a typed Js.t object, which will generate compiler errors if you try to access fields that don't exist:

Remove a field, get compiler errors

While these examples use the ReasonML syntax, using the standard OCaml syntax works as well.

Usage

Download the server schema

This plugin requires a graphql_schema.json file to exist somewhere in the project hierarchy, containing the result of sending an introspection query to your backend.

To help you with this, a simple script is included to send this query to a server and save the result as graphql_schema.json in the current directory.

yarn send-introspection-query http://my-api.example.com/api
# or, if you use npm
npm run send-introspection-query http://my-api.example.com/api

Send queries

To define a query, you declare a new module and type the query as a string inside the graphql extension:

module HeroQuery = [%graphql {|
{
  hero {
    name
  }
}
|}];

This module exposes a few functions, but the most useful one is make, which takes all arguments to the query/mutation as labelled function arguments, ending with (). It an object containing three things: query, which is a string containing the query itself; variables, a Js.Json.t object containing the serialized variables for the query; and parse, a function that takes a Js.Json.t instance and returns a typed object corresponding to the query.

A simple example might make this a bit clear:

module HeroQuery = [%graphql {| { hero { name } } |}];

/* Construct a "packaged" query; HeroQuery takes no arguments: */
let heroQuery = HeroQuery.make();

/* Send this query string to the server */
let query = heroQuery##query;

/* Let's assume that this was the result we got back from the server */
let sampleResponse = "{ \"hero\": {\"name\": \"R2-D2\"} }";

/* Convert the response to JSON and parse the result */
let result = Js.Json.parseExn(sampleResponse) |> query##parse;

/* Now you've got a well-typed object! */
Js.log("The hero of the story is " ++ result##hero##name);

Integrating with the Fetch API

bs-fetch is a wrapper around the Fetch API. I've been using this simple function to send/parse queries:

exception Graphql_error(string);

let sendQuery = q =>
  Bs_fetch.(
    fetchWithInit(
      "/graphql",
      RequestInit.make(
        ~method_=Post,
        ~body=
          Js.Dict.fromList([
            ("query", Js.Json.string(q##query)),
            ("variables", q##variables)
          ])
          |> Js.Json.object_
          |> Js.Json.stringify
          |> BodyInit.make,
        ~credentials=Include,
        ~headers=
          HeadersInit.makeWithArray([|("content-type", "application/json")|]),
        ()
      )
    )
    |> Js.Promise.then_(resp =>
         if (Response.ok(resp)) {
           Response.json(resp)
           |> Js.Promise.then_(data =>
                switch (Js.Json.decodeObject(data)) {
                | Some(obj) =>
                  Js.Dict.unsafeGet(obj, "data")
                  |> q##parse
                  |> Js.Promise.resolve
                | None =>
                  Js.Promise.reject(Graphql_error("Response is not an object"))
                }
              );
         } else {
           Js.Promise.reject(
             Graphql_error("Request failed: " ++ Response.statusText(resp))
           );
         }
       )
  );

Limitations

This implementation is incomplete. It does not support:

  • Non-inline fragments of any kind.
  • Interfaces.
  • All GraphQL validations. It will not validate argument types and do other sanity-checking of the queries. The fact that a query compiles does not mean that it will pass server-side validation.

Things that do work

  • Objects are converted into Js.t objects
  • Enums are converted into polymorphic variants
  • Floats, ints, strings, booleans, id are converted into their corresponding native OCaml types.
  • Custom scalars are parsed as Js.Json.t
  • Arguments with input objects
  • Using @skip and @include will force non-optional fields to become optional.
  • Unions are converted to polymorphic variants, with exhaustiveness checking. This only works for object types, not for unions containing interfaces.

Extra features

By using some directives prefixed bs, graphql_ppx lets you modify how the result of a query is parsed. All these directives will be removed from the query at compile time, so your server doesn't have to support them.

Record conversion

While Js.t objects often have their advantages, they also come with some limitations. For example, you can't create new objects using the spread (...) syntax or pattern match on their contents. Since they are not named, they also result in quite large type error messages when there are mismatches.

OCaml records, on the other hand, can be pattern matched, created using the spread syntax, and give nicer error messages when they mismatch. graphql_ppx gives you the option to decode a field as a record using the @bsRecord directive:

type hero = {
  name: string,
  height: number,
  mass: number
};

module HeroQuery = [%graphql {|
{
  hero @bsRecord {
    name
    height
    mass
  }
}
|}];

Note that the record has to already exist and be in scope for this to work. graphql_ppx will not create the record. Even though this involves some duplication of both names and types, type errors will be generated if there are any mismatches.

Custom field decoders

If you've got a custom scalar, or just want to convert e.g. an integer to a string to properly fit a record type (see above), you can use the @bsDecoder directive to insert a custom function in the decoder:

module HeroQuery = [%graphql {|
{
  hero {
    name
    height @bsDecoder(fn: "string_of_float")
    mass
  }
}
|}];

In this example, height will be converted from a number to a string in the result. Using the fn argument, you can specify any function literal you want.

Non-union variant conversion

If you've got an object which in practice behave like a variant - like signUp above, where you either get a user or a list of errors - you can add a @bsVariant directive to the field to turn it into a polymorphic variant:

module SignUpQuery = [%graphql
  {|
mutation($name: String!, $email: String!, $password: String!) {
  signUp(email: $email, email: $email, password: $password) @bsVariant {
    user {
      name
    }

    errors {
      field
      message
    }
  }
}
|}
];

let x =
  SignUpQuery.make(~name="My name", ~email="[email protected]", ~password="secret", ())
  |> Api.sendQuery |> Promise.then_(response =>
    switch (response##signUp) {
    | `User(user) => Js.log2("Signed up a user with name ", user##name)
    | `Errors(errors) => Js.log2("Errors when signing up: ", errors)
    } |> Promise.resolve);

This helps with the fairly common pattern for mutations that can fail with user-readable errors.

Alternative Query.make syntax

When you define a query with variables, the make function will take corresponding labelled arguments. This is convenient when constructing and sending the queries yourself, but might be problematic when trying to abstract over multiple queries.

For this reason, another function called makeWithVariables is also generated. This function takes a single Js.t object containing all variables.

module MyQuery = [%graphql {|
  mutation ($username: String!, $password: String!) {
    ...
  }
|}];

/* You can either use `make` with labelled arguments: */
let query = MyQuery.make(~username="testUser", password="supersecret", ());

/* Or, you can use `makeWithVariables`: */
let query = MyQuery.makeWithVariables({ "username": "testUser", "password": "supersecret" });

Getting the type of the parsed value

If you want to get the type of the parsed and decoded value - useful in places where you can't use OCaml's type inference - use the t type of the query module:

module MyQuery = [%graphql {| { hero { name height }} |}];


/* This is something like Js.t({ . hero: Js.t({ name: string, weight: float }) }) */
type resultType = MyQuery.t;

Future work

Core GraphQL features that need to be implemented:

  • Inline fragments
  • Fragment spreads
  • Selecting on interfaces
  • Selecting on unions
  • Input object arguments
  • Query validations (only partially implemented)
  • Explicit resolvers for custom scalars

Experimental: graphql-tag replacement

To simplify integration with e.g. Apollo, this PPX can write the query AST instead of the raw source, in a way that should be compatible with how graphql-tag works.

To enable this, change your bsconfig.json to:

{
    "ppx-flags": [
        "graphql_ppx/ppx\\ -ast-out"
    ]
}

Now, the query field will be a Js.Json.t structure instead of a string, ready to be sent to Apollo.

module HeroQuery = [%graphql {| { hero { name } } |}];

/* Construct a "packaged" query; HeroQuery takes no arguments: */
let heroQuery = HeroQuery.make();

/* This is no longer a string, but rather an object structure */
let query = heroQuery##query;

Building manually on unsupported platforms

graphql_ppx supports 64 bit Linux, Windows, and macOS, as well as 32 bit Windows out of the box. If you're on any other platform, please open an issue on this repository so we can support it.

graphql_ppx's People

Contributors

anmonteiro avatar mhallin avatar szymonzmyslony avatar ulrikstrid avatar wokalski avatar wyze avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

maranda1563

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.