Giter Club home page Giter Club logo

apollo-link-json-api's Introduction

JSON API Link Build Status codecov

Purpose

An Apollo Link to easily use GraphQL with a JSON API compliant server.

Built on top of apollo-link-rest. If you have a non-JSON API REST service, check that out as an alternative.

Installation

npm install apollo-link-json-api apollo-link graphql graphql-anywhere qs humps --save

# or

yarn add apollo-link-json-api apollo-link graphql graphql-anywhere qs humps

apollo-link, graphql, qs, humps, and graphql-anywhere are peer dependencies needed by apollo-link-json-api.

Usage

Basics

import { JsonApiLink } from "apollo-link-json-api";
// Other necessary imports...

// Create a JsonApiLink for the JSON API
// If you are using multiple link types, jsonApiLink should go before httpLink,
// as httpLink will swallow any calls that should be routed through jsonApi!
const jsonApiLink = new JsonApiLink({
  uri: 'http://jsonapiplayground.reyesoft.com/v2/',
});

// Configure the ApolloClient with the default cache and JsonApiLink
const client = new ApolloClient({
  link: jsonApiLink,
  cache: new InMemoryCache(),
});

// A simple query to retrieve data about the first author
const query = gql`
  query firstAuthor {
    author @jsonapi(path: "authors/1") {
      name
    }
  }
`;

// Invoke the query and log the person's name
client.query({ query }).then(response => {
  console.log(response.data.name);
});

Advanced Querying

JSON API Link supports unpacking related resources into a friendlier GraphQL query structure.

const query = gql`
  query firstAuthor {
    author @jsonapi(path: "authors/1?include=series,series.books") {
      name
      series {
        title
        books {
          title
        }
      }
    }
  }
`;

While JSON API Link does support running multiple nested queries, prefer sideloading resources in a single request by using the ?include parameter if your JSON API server supports it.

// Avoid this
const badQuery = gql`
  query firstAuthor {
    author @jsonapi(path: "authors/1") {
      name
      series @jsonapi(path: "authors/1/series") {
        title
      }
    }
  }
`;

// Prefer this
const query = gql`
  query firstAuthor {
    author @jsonapi(path: "authors/1?include=series") {
      name
      series {
        title
      }
    }
  }
`;

Mutations

import React from 'react'
import gql from 'graphql-tag'
import { Mutation } from 'react-apollo'

export const UPDATE_BOOK_TITLE = gql`
  mutation UpdateBookTitle($input: UpdateBookTitleInput!) {
    book(input: $input) @jsonapi(path: "/books/{args.input.data.id}", method: "PATCH") {
      title
    }
  }
`

const UpdateBookTitleButton = ({ bookId }) => (
  <Mutation
    mutation={UPDATE_BOOK_TITLE}
    update={(store, { data: { book } }) => {
      // Update your Apollo cache with result
      console.log(book.title)
    }}
  >
    {mutate => (
      <button onClick={() => 
        mutate({
          variables: {
            input: {
              data: {
                id: bookId,
                type: 'books',
                attributes: { title: 'Changed title!' }
              }
            }
          },
          optimisticResponse: {
            book: {
              __typename: 'books',
              title: 'Changed title!'
            }
          }
        })
        }>
        Update your book title!
        </button>
    )}
  </Mutation>
)

Options

JSON API Link takes an object with some options on it to customize the behavior of the link. The options you can pass are outlined below:

  • uri: the URI key is a string endpoint (optional when endpoints provides a default)
  • endpoints: root endpoint (uri) to apply paths to or a map of endpoints
  • customFetch: a custom fetch to handle API calls
  • headers: an object representing values to be sent as headers on the request
  • credentials: a string representing the credentials policy you want for the fetch call
  • fieldNameNormalizer: function that takes the response field name and converts it into a GraphQL compliant name
  • fieldNameDenormalizer: function that takes the JavaScript object key name and converts it into a JSON API compliant name
  • typeNameNormalizer: function that takes the JSON API resource type and converts it to a GraphQL __typename.

Context

JSON API Link uses the headers field on the context to allow passing headers to the HTTP request. It also supports the credentials field for defining credentials policy.

  • headers: an object representing values to be sent as headers on the request
  • credentials: a string representing the credentials policy you want for the fetch call

Accessing metadata and links

By default, this library flattens your server response. If you need to access values that are unavailable by this simple querying method, you can add includeJsonapi: true to your @jsonapi directive, which will instead return the flattened "GraphQL-like" structure under a graphql key, and the original response structure under a jsonapi key. Resources are still nested in a tree structure under the jsonapi key, but data/attribute/relatioship keys are not flattened out.

query authorsWithMeta {
  authors @jsonapi(path: "authors?include=series", includeJsonapi: true) {
    graphql {
      name
      series {
        title
      }
    }

    jsonapi {
      meta {
        pageCount
      }
      links {
        first
        last
        current
      }
      // The resource data is available here, though it's probably easier to
      // grab from the `graphql` structure
      data {
        attributes {
          name
        }
        relationships {
          series {
            data {
              attributes {
                title
              }
            }
            links {
              related
            }
          }
        }
      }
    }
  }
}

Contributing

This project uses TypeScript to bring static types to JavaScript and uses Jest for testing. To get started, clone the repo and run the following commands:

npm install # or `yarn`

npm test # or `yarn test` to run tests
npm test -- --watch # run tests in watch mode

npm run check-types # or `yarn check-types` to check TypeScript types

To run the library locally in another project, you can do the following:

npm link

# in the project you want to run this in
npm link apollo-link-json-api

apollo-link-json-api's People

Contributors

batman-api-graphql avatar d1no avatar damusnet avatar drueck avatar emmenko avatar epitaphmike avatar fabien0102 avatar fbartho avatar gforrest-bw avatar heyhugo avatar homburg avatar hwillson avatar ivank avatar jsjoeio avatar luhugo avatar marnusw avatar milesj avatar nabilnaffar avatar nderscore avatar nrcloud avatar paulpdaniels avatar peggyrayzis avatar petetnt avatar pgilad avatar rsullivan00 avatar sabativi avatar skovy avatar sky-franciscogoncalves avatar valerybugakov avatar yoshi415 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

apollo-link-json-api's Issues

Links support

Right now, links are completely removed.

We could consider at least returning them so they can be used for mutations or the like without being route aware.

Cannot read property 'attributes' of undefined

I'm trying to give this library a test spin and an error is thrown that it can't "read property attributes of undefined." I see the data is fetched successfully. It looks like the method "flattenResource" is called twice. The first time it has data but on the second it is undefined. Maybe it's something with my setup. Is there a working example available that I could reference?

I've created a codesandbox to demonstrate my issue.

Improve pagination support

Pagination can be currently supported in a limited fashion by manually paging outside of apollo-link-json-api with component state:

import React from 'react';
import gql from 'graphql-tag';
import { Query } from 'react-apollo';

const AUTHORS_QUERY = gql`
  query AuthorsQuery($page: Integer!) {
    authors(page: $page) @jsonapi(path: "/authors?page={args.page}") {
      id
    }
  }
`;

const AuthorsList = () => {
  const [page, setPage] = useState(1);

  return (
    <Query query={AUTHORS_QUERY} variables={{ page: 1 }}>
      {({ loading, data, fetchMore }) => {
        if (loading) {
          return 'loading';
        }
        return (
          <>
            <div>
              {data.authors.map(({ id }) => (
                <div>Author {id} loaded</div>
              ))}
            </div>
            <button
              onClick={() => {
                setPage(page + 1);
                fetchMore({
                  variables: { page: page + 1 },

                  updateQuery: (prev, { fetchMoreResult }) => {
                    if (!fetchMoreResult) return prev;
                    return {
                      authors: [...prev.authors, ...fetchMoreResult.authors],
                    };
                  },
                });
              }}
            >
              Moar
            </button>
          </>
        );
      }}
    </Query>
  );
};

export default AuthorsList;

This sort of works, but with this pagination setup, we should be able to know when we are done loading resources, yet that isn't available through apollo-link-json-api.

In the JSON APIs I currently work with, that pagination information is returned in the top-level meta and links fields in a list request. For example, a request for GET /authors?page=1 would return

{
  data: [...],
  meta: { record_count: 123 },
  links: {
    first: 'https://example.com/authors?page=1',
    next: 'https://example.com/authors?page=2',
    last: 'https://example.com/authors?page=5'
  }
}

And the links.next key will no longer be available if we are on the last page.

Treat 422 http errors as graphql errors

I think 422 http errors should be treated as graphql errors.
Right now if there's a 422 http error then it's treated as networkError, but instead we probably want it as graphQLErrors so the error can be gracefully handled.

Probably safe to check if the response has errors field to normalize the errors and move the operation forward.

Screen Shot 2019-06-14 at 1 12 20 PM

`Headers` is not defined when running in SSR

Hi! Very cool library. I'm trying to get it working with a Next.js App which does querying both client and server side. I'm running into this issue:

ReferenceError: Headers is not defined
at normalizeHeaders (project/node_modules/src/jsonApiLink.ts:593:26)

which leads me to this snippet:

/**
 * Helper that makes sure our headers are of the right type to pass to Fetch
 */
export const normalizeHeaders = (
  headers: JsonApiLink.InitializationHeaders,
): Headers => {
  // Make sure that our headers object is of the right type
  if (headers instanceof Headers) {
    return headers;
  } else {
    return new Headers(headers || {});
  }
};

The problem is this: to create an instance of Headers on the server, one must use Headers from node-fetch like so:

import { Headers } from 'node-fetch';

const customHeaders = new Headers({
  'User-Agent': 'My App',
  Authorization: `Bearer ${MY_TOKEN}`,
});

relying on the implicit Headers global causes this to fail. would you be open to making headers more permissive?

Support __typename global customization (prefixing)

To support using alongside GraphQL APIs with the same typenames, we should support a global option to transform the jsonapi type into a GraphQL __typename.

Ideally this would be a function so the user has full control, but we immediately only need the ability to add the same prefix to all the autogenerated types.

For example, something like

const jsonApiLink = new JsonApiLink({
  uri: 'http://jsonapiplayground.reyesoft.com/v2/',
  typeNormalizer: type => `My${pascalize(type)}`
});

would convert resource

{ data: {
  id: "123",
  type: "books",
  attributes: {}
}}

to have __typename MyBooks.

Return an empty list when `include` is given but there's no data from `included`

There's an issue when a graphql query/mutation expects a field which is a jsonapi relationship(data in included) but no data is given from the response.
I think the expected behavior here would be returning an empty list.

Sample error message:

Error: Error writing result to store for query:
 {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateReel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateReelInput"}}}}],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"directives":[{"kind":"Directive","name":{"kind":"Name","value":"jsonapi"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"StringValue","value":"/reels/{args.input.data.id}?include=reel_video_clips","block":false}},{"kind":"Argument","name":{"kind":"Name","value":"method"},"value":{"kind":"StringValue","value":"PATCH","block":false}}]}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"name"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"publishStatus"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"reelVideoClips"},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"},"arguments":[],"directives":[]},{"kind":"FragmentSpread","name":{"kind":"Name","value":"VideoClip_videoClip"},"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}},{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"VideoClip_videoClip"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"VIReelVideoClips"}},"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"name"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"startTime"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"endTime"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}}],"loc":{"start":0,"end":421}}
__typename is undefined bundle.esm.js:704

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.