GraphQL

Our GraphQL API can be explored via GraphiQL at your instance's /-/graphql-explorer or at GitLab.com.

You can check all existing queries and mutations on the right side of GraphiQL in its Documentation explorer. It's also possible to write queries and mutations directly on the left tab and check their execution by clicking Execute query button on the top left:

GraphiQL interface

We use Apollo and Vue Apollo for working with GraphQL on the frontend.

Apollo Client

To save duplicated clients getting created in different apps, we have a default client that should be used. This setups the Apollo client with the correct URL and also sets the CSRF headers.

Default client accepts two parameters: resolvers and config.

  • resolvers parameter is created to accept an object of resolvers for local state management queries and mutations
  • config parameter takes an object of configuration settings:
    • cacheConfig field accepts an optional object of settings to customize Apollo cache
    • baseUrl allows us to pass a URL for GraphQL endpoint different from our main endpoint (i.e.${gon.relative_url_root}/api/graphql)
    • assumeImmutableResults (set to false by default) - this setting, when set to true, will assume that every single operation on updating Apollo Cache is immutable. It also sets freezeResults to true, so any attempt on mutating Apollo Cache will throw a console warning in development environment. Please ensure you're following the immutability pattern on cache update operations before setting this option to true.

GraphQL Queries

To save query compilation at runtime, webpack can directly import .graphql files. This allows webpack to preprocess the query at compile time instead of the client doing compilation of queries.

To distinguish queries from mutations and fragments, the following naming convention is recommended:

  • allUsers.query.graphql for queries;
  • addUser.mutation.graphql for mutations;
  • basicUser.fragment.graphql for fragments.

Fragments

Fragments are a way to make your complex GraphQL queries more readable and re-usable. Here is an example of GraphQL fragment:

fragment DesignListItem on Design {
  id
  image
  event
  filename
  notesCount
}

Fragments can be stored in separate files, imported and used in queries, mutations or other fragments.

#import "./designList.fragment.graphql"
#import "./diffRefs.fragment.graphql"

fragment DesignItem on Design {
  ...DesignListItem
  fullPath
  diffRefs {
    ...DesignDiffRefs
  }
}

More about fragments: GraphQL Docs

Usage in Vue

To use Vue Apollo, import the Vue Apollo plugin as well as the default client. This should be created at the same point the Vue application is mounted.

import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);

const apolloProvider = new VueApollo({
  defaultClient: createDefaultClient(),
});

new Vue({
  ...,
  apolloProvider,
  ...
});

Read more about Vue Apollo in the Vue Apollo documentation.

Local state with Apollo

It is possible to manage an application state with Apollo by passing in a resolvers object when creating the default client. The default state can be set by writing to the cache after setting up the default client.

import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);

const defaultClient = createDefaultClient({
  Query: {
    ...
  },
  Mutations: {
    ...
  },
});

defaultClient.cache.writeData({
  data: {
    isLoading: true,
  },
});

const apolloProvider = new VueApollo({
  defaultClient,
});

Read more about local state management with Apollo in the Vue Apollo documentation.

Testing

Mocking response as component data

With Vue test utils it is easy to quickly test components that fetch GraphQL queries. The simplest way is to use shallowMount and then set the data on the component

it('tests apollo component', () => {
  const vm = shallowMount(App);

  vm.setData({
    ...mock data
  });
});

Testing loading state

If we need to test how our component renders when results from the GraphQL API are still loading, we can mock a loading state into respective Apollo queries/mutations:

  function createComponent({
    loading = false,
  } = {}) {
    const $apollo = {
      queries: {
        designs: {
          loading,
        },
    };

    wrapper = shallowMount(Index, {
      sync: false,
      mocks: { $apollo }
    });
  }

  it('renders loading icon', () => {
  createComponent({ loading: true });

  expect(wrapper.element).toMatchSnapshot();
})

Testing Apollo components

If we use ApolloQuery or ApolloMutation in our components, in order to test their functionality we need to add a stub first:

import { ApolloMutation } from 'vue-apollo';

function createComponent(props = {}) {
  wrapper = shallowMount(MyComponent, {
    sync: false,
    propsData: {
      ...props,
    },
    stubs: {
      ApolloMutation,
    },
  });
}

ApolloMutation component exposes mutate method via scoped slot. If we want to test this method, we need to add it to mocks:

const mutate = jest.fn(() => Promise.resolve());
const $apollo = {
  mutate,
};

function createComponent(props = {}) {
  wrapper = shallowMount(MyComponent, {
    sync: false,
    propsData: {
      ...props,
    },
    stubs: {
      ApolloMutation,
    },
    mocks: {
      $apollo:
    }
  });
}

Then we can check if mutate is called with correct variables:

const mutationVariables = {
  mutation: createNoteMutation,
  update: expect.anything(),
  variables: {
    input: {
      noteableId: 'noteable-id',
      body: 'test',
      discussionId: '0',
    },
  },
};

it('calls mutation on submitting form ', () => {
  createComponent()
  findReplyForm().vm.$emit('submitForm');

  expect(mutate).toHaveBeenCalledWith(mutationVariables);
});

Usage outside of Vue

It is also possible to use GraphQL outside of Vue by directly importing and using the default client with queries.

import defaultClient from '~/lib/graphql';
import query from './query.graphql';

defaultClient.query(query)
  .then(result => console.log(result));

Read more about the Apollo client in the Apollo documentation.