Marmelab Blog

Admin-on-rest now supports GraphQL backends

Admin-on-rest is our open source React admin GUI for REST APIs, which has recently been released in version 1.1. In a nutshell, it's a single-page application providing the views required to Create, Retrieve, Update, and Delete (CRUD) REST resources, with the great usability of the Material Design UI kit.

From the very beginning, it has been possible to provide your own REST client implementation. Many such implementations already exist, contributed either by us, or by the community:

You might also know about GraphQL, a query language for APIs, often presented as the successor of REST. If you haven't, I advise you to read this article by Sacha Greif: So what’s this GraphQL thing I keep hearing about?.

Introducing aor-simple-graphql-client

It is now possible to use a GraphQL server with Admin-on-rest:

npm install aor-simple-graphql-client

A version of the admin-on-rest demo using this client: marmelab.com/admin-on-rest-graphql-demo.

The source code for this demo is available on GitHub at marmelab/admin-on-rest-graphql-demo.

The GraphQL client

Here is an example of how to register this client with an admin-on-rest application.

import React, { Component } from 'react';
import { buildApolloClient } from 'aor-simple-graphql-client';

import { Admin, Resource } from 'admin-on-rest';
import { Delete } from 'admin-on-rest/lib/mui';

import { PostCreate, PostEdit, PostList } from '../components/admin/posts';

class AdminApp extends Component {
    constructor() {
        super();
        this.state = { restClient: null };
    }
    componentDidMount() {
        // We are using state here because the apollo client initialization is asynchronous
        buildApolloClient().then(restClient => this.setState({ restClient }));
    }

    render() {
        const { restClient } = this.state;

        if (!restClient) {
            return <div>Loading</div>;
        }

        return (
            <Admin restClient={restClient}>
                <Resource name="Post" list={PostList} edit={PostEdit} create={PostCreate} remove={Delete} />
            </Admin>
        );
    }
}

export default AdminApp;

We chose to use the awesome apollo stack to communicate with the GraphQL endpoint.

By default, our Admin-on-rest client will instantiate a new Apollo client. It is of course possible to specify the endpoint's url or provide a custom Apollo client if further customization is needed:

import React, { Component } from 'react';
import { ApolloClient, createNetworkInterface } from 'apollo-client';
import { buildApolloClient } from 'aor-simple-graphql-client';

import { Admin, Resource } from 'admin-on-rest';
import { Delete } from 'admin-on-rest/lib/mui';

import { PostCreate, PostEdit, PostList } from '../components/admin/posts';

const client = new ApolloClient();

class AdminApp extends Component {
    constructor() {
        super();
        this.state = { restClient: null };
    }
    componentDidMount() {
        const client = new ApolloClient({
            networkInterface: createNetworkInterface({
                uri: 'https://api.com/graphql',
            });

        buildApolloClient({ client }).then(restClient => this.setState({ restClient }));
    }

    render() {
        const { restClient } = this.state;

        if (!restClient) {
            return <div>Loading</div>;
        }

        return (
            <Admin restClient={restClient}>
                <Resource name="Post" list={PostList} edit={PostEdit} create={PostCreate} remove={Delete} />
            </Admin>
        );
    }
}

export default AdminApp;

Introspection

By default, the admin-on-rest client will run an introspection query on the GraphQL endpoint to automatically discover the available types, queries and mutations.

In order to communicate with admin-on-rest, a rest client should implement the following REST verbs::

  • GET_LIST
  • GET_MANY
  • GET_MANY_REFERENCE
  • GET_ONE
  • CREATE
  • UPDATE
  • DELETE

To facilitate the client usage, the client expects the queries and mutations declared in the server schema to follow a few conventions:

  • GET_LIST, GET_MANY and GET_MANY_REFERENCE result in calls to queries named getPageOf[RESOURCE] where RESOURCE is the pluralized version of the resource name (we use pluralize). For example: getPageOfProducts. Refer to this query's documentation for details about its parameters.

  • GET_ONE results in calls to queries named get[RESOURCE] where RESOURCE is the resource name. For example: getProduct. Refer to this query's documentation for details about its parameters.

  • CREATE results in calls to queries named create[RESOURCE] where RESOURCE is the resource name. For example: createProduct. Refer to this query's documentation for details about its parameters.

  • UPDATE results in calls to queries named update[RESOURCE] where RESOURCE is the resource name. For example: updateProduct. Refer to this query's documentation for details about its parameters.

  • DELETE results in calls to queries named remove[RESOURCE] where RESOURCE is the resource name. For example: removeProduct. Refer to this query's documentation for details about its parameters.

As it is possible to have read-only resources (only list and detail pages), the only requirement is to have at least the getPageOf and get queries implemented for a resource to be usable.

All discovered queries and mutations must have their matching resolvers implemented on the GraphQL endpoint.

Customization

For introspection, it is possible to include or exclude specific types, queries and mutations:

// In getGraphqlClient.js
import { buildApolloClient } from 'aor-simple-graphql-client';

export default buildApolloClient({
    introspection: {
        includeTypes: ['Product', 'Category', 'Customer', 'Command'],
    },
});

Furthermore, if some introspected queries or mutations are incorrect or incomplete, custom queries and mutations can be provided to replace them. For example, the current version of the client is unable to handle embedded objects and relations. If a type in the schema has such properties, the related queries will fail. For those cases, we added the possibility to override the queries discovered through introspection:

// In getGraphqlClient.js
import { buildApolloClient } from 'aor-simple-graphql-client';
import gql from 'graphql-tag';

export default buildApolloClient({
    queries: {
        Command: {
            GET_LIST: gql`
                query getPageOfCommands($page: Int, $perPage: Int, $sortField: String, $sortOrder: String, $filter: String) {
                    getPageOfCommands(page: $page, perPage: $perPage, sortField: $sortField, sortOrder: $sortOrder, filter: $filter) {
                        items {
                            id
                            reference
                            customer_id
                            total
                            status
                            basket { product_id, quantity }
                        }
                        totalCount
                    }
                }
            `,
            GET_ONE: gql`
                query getCommand($id: ID!) {
                    getCommand(id: $id) {
                        id
                        reference
                        customer_id
                        total
                        status
                        basket { product_id, quantity }
                    }
                }
            `,
        }
    },
});

Finally, introspection can be disabled completely, in which case, queries and mutations must be provided:

// In getGraphqlClient.js
import { buildApolloClient } from 'aor-simple-graphql-client';
import gql from 'graphql-tag';

export default buildApolloClient({
    introspection: false,
    queries: {
        Command: {
            GET_LIST: gql`
                query getPageOfCommands($page: Int, $perPage: Int, $sortField: String, $sortOrder: String, $filter: String) {
                    getPageOfCommands(page: $page, perPage: $perPage, sortField: $sortField, sortOrder: $sortOrder, filter: $filter) {
                        items {
                            id
                            reference
                            customer_id
                            total
                            status
                            basket { product_id, quantity }
                        }
                        totalCount
                    }
                }
            `,
            GET_ONE: gql`
                query getCommand($id: ID!) {
                    getCommand(id: $id) {
                        id
                        reference
                        customer_id
                        total
                        status
                        basket { product_id, quantity }
                    }
                }
            `,
            CREATE: gql`
                mutation createCommand($data: String!) {
                    createCommand(data: $data) {
                        id
                        reference
                        customer_id
                        total
                        status
                        basket { product_id, quantity }
                    }
                }
            `,
            UPDATE: gql`
                mutation updateCommand($data: String!) {
                    updateCommand(data: $data) {
                        id
                        reference
                        customer_id
                        total
                        status
                        basket { product_id, quantity }
                    }
                }
            `,
        }
    },
});

GraphQL Flavors

Our convention for GraphQL queries and mutations may not be yours. If you're using a service such as GraphCool, you will notice they chose to implement pagination the mongo way (skip and limit) rather than with page index and size. Furthermore, instead of returning a totalCount field in the query result for a page of data, they included it in the query results meta.

For those use cases, it is possible to supply a custom GraphQL flavor. A flavor acts as the translator between admin-on-rest requests and your GraphQL queries and mutations.

This is useful if you want more control over which paramaters are sent from Admin-on-rest to your GraphQL backend, and how they are sent.

A flavor is an object with a key for each rest action defined by Admin-on-rest: GET_ONE, GET_LIST, GET_MANY, GET_MANY_REFERENCE, CREATE, UPDATE and DELETE.

For each of these actions, it defines:

  • how the name of the operation (query or mutation) can be inferred during introspection
  • how the query will be generated through introspection
  • how parameters are translated from admin-on-rest to Apollo
  • how the query results from Apollo to admin-on-rest are parsed

For example, this is the GET_LIST definition in the default flavor:

export default {
    [GET_LIST]: {
        operationName: resourceType => `getPageOf${pluralize(resourceType.name)}`,
        getParameters: params => ({
            filter: JSON.stringify(params.filter),
            page: params.pagination.page - 1,
            perPage: params.pagination.perPage,
            sortField: params.sort.field,
            sortOrder: params.sort.order,
        }),
    },
    ...
};

To use a custom GraphQL flavor, pass it in the options:

// In getGraphqlClient.js
import { buildApolloClient } from 'aor-simple-graphql-client';
import myCustomFlavor from './myCustomFlavor';

export default buildApolloClient({ flavor: myCustomFlavor });

A Note About The Demo

The demo currently uses a fake GraphQL server using graphql-tools mocking utilities to retrieve data from a simple json file. A future version will use a real GraphQL backend probably powered by a serverless architecture.

Conclusion

It has been really fun to work on this GraphQL client. Apollo has proved to be very easy to understand and use. We do believe those are breakthrough technologies and we will work on some other projects around them:

  • FakeGraphQL which will allow to get a mocked GraphQL endpoint with data from a json file: FakeRest for GraphQL!
  • aor-realtime which enables real time updates inside admin-on-rest and will be used by the graphql client.

This client is at its very early stage. The first step was to demonstrate admin-on-rest is not limited to REST endpoints. We do have plans to make it better: real time updates, subscriptions, etc. It might also be interesting to extract the introspection mechanism to its own project.

Those projects are open source: please get in touch with us to make them better!