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:
- aor-json-rest-client
- aor-loopback for Loopback
- aor-feathers-client for Feathers
- aor-parseserver-client for Parse
- aor-jsonapi-client for JSONAPI
- aor-postgrest-client for postgREST
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
andGET_MANY_REFERENCE
result in calls to queries namedgetPageOf[RESOURCE]
whereRESOURCE
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 namedget[RESOURCE]
whereRESOURCE
is the resource name. For example:getProduct
. Refer to this query's documentation for details about its parameters.CREATE
results in calls to queries namedcreate[RESOURCE]
whereRESOURCE
is the resource name. For example:createProduct
. Refer to this query's documentation for details about its parameters.UPDATE
results in calls to queries namedupdate[RESOURCE]
whereRESOURCE
is the resource name. For example:updateProduct
. Refer to this query's documentation for details about its parameters.DELETE
results in calls to queries namedremove[RESOURCE]
whereRESOURCE
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!
- Repository for Admin-on-rest
- Repository for aor-simple-graphql-client
- Repository for aor-realtime