npm package: @hburbano/ra-data-hasura-graphql
Description: A data provider for connecting react-admin to a Hasura endpoint
Latest version: 0.1.15

# ra-data-hasura-graphql

A GraphQL data provider for [react-admin](https://marmelab.com/react-admin) tailored to target [Hasura](https://hasura.io/) GraphQL endpoints.

-   [Installation](#installation)
-   [Usage](#installation)
-   [Options](#options)

Example application demonstrating usage: [react-admin-low-code](https://github.com/cpursley/react-admin-low-code)

## Benifits and Motivation

This utility is built on top of [ra-data-graphql](https://github.com/vladimiregorov/react-admin/blob/master/packages/ra-data-graphql/README.md) and is a custom data provider for the current Hasura graphql API format.

The existing ra-data-hasura provider communicates with Hasura V1, using standard REST and not GraphQL. The existing ra-data-graphql-simple provider, requires that your graphql endpoint implement a specific grammar for the objects and methods exposed, which is not an option available if you intend to use a Hasura API because the exposed objects and methods are generated to match their own specification.

This utility auto generates valid graphql queries based on the properties exposed by the Hasura API such as `object_bool_exp` and `object_set_input`.

## Installation

Install with:

```sh
npm install --save graphql ra-data-hasura-graphql
```

## Usage

The `ra-data-hasura-graphql` package exposes a single function, which is a constructor for a `dataProvider` based on a Hasura GraphQL endpoint. When executed, this function calls the endpoint, running an [introspection](http://graphql.org/learn/introspection/) query to learn about the specific data models exposed by your Hasura endpoint. It uses the result of this query (the GraphQL schema) to automatically configure the `dataProvider` accordingly.

```jsx
// in App.js
import React, { Component } from 'react';
import buildHasuraProvider from 'ra-data-hasura-graphql';
import { Admin, Resource } from 'react-admin';

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

class App extends Component {
    constructor() {
        super();
        this.state = { dataProvider: null };
    }
    componentDidMount() {
        buildHasuraProvider({
            clientOptions: { uri: 'http://localhost:4000' },
        }).then((dataProvider) => this.setState({ dataProvider }));
    }

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

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

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

export default App;
```

The adapter generates queries based on the standard API's and query arguments generated by Hasura.
For example, a `GET_LIST` request for a person resource, with the parameters :

```json
{
    "pagination": { "page": 1, "perPage": 5 },
    "sort": { "field": "name", "order": "DESC" },
    "filter": {
        "ids": [
            "f10b7d4b-72a2-4cca-a978-55577e3ef35c",
            "fe7470ae-2626-48c7-80dc-1726c066ce4d"
        ]
    }
}
```

Generates the following graphql request for Hasura :

```
query person($limit: Int, $offset: Int, $order_by: [person_order_by!]!, $where: person_bool_exp) {
  items: person(limit: $limit, offset: $offset, order_by: $order_by, where: $where) {
    address_id
    id
    name
  }
  total: person_aggregate(limit: $limit, offset: $offset, order_by: $order_by, where: $where) {
    aggregate {
      count
    }
  }
}

And generates the following variables to be passed along side the query:
{
  limit: 5,
  offset: 0,
  order_by: { name: 'desc' },
  where: {
    _and: [
      {
        id: {
          _in: ['f10b7d4b-72a2-4cca-a978-55577e3ef35c','fe7470ae-2626-48c7-80dc-1726c066ce4d']
        }
      }
    ]
  }
}

```

### Sorting

Dot notation is supported for sort fields, and will be expanded into an object in the graphQL query.

For example:

```
export const PostList = (props) => (
    <List {...props} sort={{ field: 'user.email', order: 'DESC' }}>
        ...
    </List>
);
```

Will generate the following graphQL query:

```
{
  limit: 25,
  offset: 0,
  order_by: { user: { email: 'desc' } }
}
```

### Authentication

To send authentication headers, declare your own apollo client.

```
import ApolloClient from 'apollo-boost';

const client = new ApolloClient({
  uri: 'http://localhost:8080/v1/graphql',
  headers: {
    'x-hasura-admin-secret': `myadminsecretkey`,
    // 'Authorization': `Bearer xxxx`,
  }
});

// When building your provider inside your component
// set up the client like this
buildHasuraProvider({ client })
```

## Options

### Customize the Apollo client

You can either supply the client options by calling `buildGraphQLProvider` like this:

```js
buildGraphQLProvider({
    clientOptions: { uri: 'http://localhost:4000', ...otherApolloOptions },
});
```

Or supply your client directly with:

```js
buildGraphQLProvider({ client: myClient });
```

### Customize the introspection

These are the default options for introspection:

```js
const introspectionOptions = {
    include: [], // Either an array of types to include or a function which will be called for every type discovered through introspection
    exclude: [], // Either an array of types to exclude or a function which will be called for every type discovered through introspection
};

// Including types
const introspectionOptions = {
    include: ['Post', 'Comment'],
};

// Excluding types
const introspectionOptions = {
    exclude: ['CommandItem'],
};

// Including types with a function
const introspectionOptions = {
    include: (type) => ['Post', 'Comment'].includes(type.name),
};

// Including types with a function
const introspectionOptions = {
    exclude: (type) => !['Post', 'Comment'].includes(type.name),
};
```

**Note**: `exclude` and `include` are mutually exclusives and `include` will take precendance.

**Note**: When using functions, the `type` argument will be a type returned by the introspection query. Refer to the [introspection](http://graphql.org/learn/introspection/) documentation for more information.

Pass the introspection options to the `buildApolloProvider` function:

```js
buildApolloProvider({ introspection: introspectionOptions });
```

## Customize fields, variables, responseParser

Using the factories you can customize the behavior of the data provider.

Just change the `buildQuery` property in the options object:

```js
import buildDataProvider from 'ra-data-hasura-graphql';
import { buildQueryFactory } from 'ra-data-hasura-graphql/src/buildQuery';
import buildVariables from 'ra-data-hasura-graphql/src/buildVariables';
import {
    buildGqlQuery,
    buildFields,
    buildMetaArgs,
    buildArgs,
    buildApolloArgs,
} from 'ra-data-hasura-graphql/src/buildGqlQuery';
import getResponseParser from 'ra-data-hasura-graphql/src/getResponseParser';

const buildGqlQueryCustom = (iR) =>
    buildGqlQuery(iR, buildFields, buildMetaArgs, buildArgs, buildApolloArgs);
const buildQuery = buildQueryFactory(
    buildVariables,
    buildGqlQueryCustom,
    getResponseParser
);
buildDataProvider({ ...opt, buildQuery });
```

### Example: Query related entities

```jsx
import buildDataProvider from 'ra-data-hasura-graphql';
import { buildQueryFactory } from 'ra-data-hasura-graphql/src/buildQuery';
import buildVariables from 'ra-data-hasura-graphql/src/buildVariables';
import {
    buildGqlQuery,
    buildFields,
    buildMetaArgs,
    buildArgs,
    buildApolloArgs,
} from 'ra-data-hasura-graphql/src/buildGqlQuery';
import getResponseParser from 'ra-data-hasura-graphql/src/getResponseParser';
import * as gqlTypes from 'graphql-ast-types-browser';

const buildFieldsCustom = (type) => {
    let res = buildFields(type);
    if (type.name === 'app') {
        // here we add additional fields we want to query for apps.
        // we are using the graphql-ast-types functions which is ast representation for graphql
        res.push(
            gqlTypes.field(
                gqlTypes.name('app_berechtigungs'),
                null,
                null,
                null,
                gqlTypes.selectionSet([
                    gqlTypes.field(gqlTypes.name('berechtigung_id')),
                    gqlTypes.field(
                        gqlTypes.name('berechtigung'),
                        null,
                        null,
                        null,
                        gqlTypes.selectionSet([
                            gqlTypes.field(gqlTypes.name('name')),
                        ])
                    ),
                ])
            )
        );
    }
    return res;
};
const buildGqlQueryCustom = (iR) =>
    buildGqlQuery(
        iR,
        buildFieldsCustom,
        buildMetaArgs,
        buildArgs,
        buildApolloArgs
    );
const buildQuery = buildQueryFactory(
    buildVariables,
    buildGqlQueryCustom,
    getResponseParser
);
buildDataProvider({ buildQuery });
```

This should add the relation to the GraphQL query and result in a query similar to:

```
app {
  id
  name
  app_berechtigungs {
    berechtigung_id
    berechtigung {
      name
    }
  }
}
```

### Special Filter Feature

This adapter allows filtering several columns at a time with using specific comparators, e.g. `ilike`, `like`, `eq`, etc.

```tsx
<Filter {...props}>
    <TextInput
        label="Search"
        source="email,first_name@_eq,last_name@_like"
        alwaysOn
    />
</Filter>
```

It will generate the following filter payload

```json
{
    "variables": {
        "where": {
            "_and": [],
            "_or": [
                {
                    "email": {
                        "_ilike": "%edu%"
                    }
                },
                {
                    "first_name": {
                        "_eq": "edu"
                    }
                },
                {
                    "last_name": {
                        "_like": "%edu%"
                    }
                }
            ]
        },
        "limit": 10,
        "offset": 0,
        "order_by": {
            "id": "asc"
        }
    }
}
```

The adapter assigns default comparator depends on the data type if it is not provided.
For string data types, it assumes as text search and uses `ilike` otherwise it uses `eq`.
For string data types that uses `like` or `ilike` it automatically transform the filter `value` as `%value%`.