npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aginix/ra-data-hasura

v0.7.2

Published

A data provider for connecting react-admin to a Hasura endpoint

Readme

ra-data-hasura

A GraphQL data provider for react-admin tailored to target Hasura GraphQL endpoints.

| Library version | React Admin version | | --------------- | ------------------- | | >= 0.7.0 | v5 | | 0.5.x – 0.6.x | v4 | | <= 0.4.2 | v3 |

Example applications demonstrating usage:

Benefits and Motivation

This utility is built on top of ra-data-graphql and is a custom data provider for the current Hasura GraphQL API format.

The existing ra-data-graphql-simple provider requires that your GraphQL endpoint implement a specific grammar for the objects and methods exposed, which is different with Hasura because the exposed objects and methods are generated differently.

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

npm install --save graphql @aginix/ra-data-hasura

Usage

The @aginix/ra-data-hasura package exposes a single default function with the following signature:

buildHasuraProvider(
  options?: Object,
  buildGqlQueryOverrides?: Object,
  customBuildVariables?: Function,
  customGetResponseParser?: Function,
) => Promise<DataProvider>

See the Options and Customizing queries sections below for more details on these arguments.

This function acts as a constructor for a dataProvider based on a Hasura GraphQL endpoint. When executed, this function calls the endpoint, running an 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.

import React, { useState, useEffect } from 'react';
import buildHasuraProvider from '@aginix/ra-data-hasura';
import { Admin, Resource } from 'react-admin';

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

const App = () => {
  const [dataProvider, setDataProvider] = useState(null);

  useEffect(() => {
    const buildDataProvider = async () => {
      const dataProvider = await buildHasuraProvider({
        clientOptions: { uri: 'http://localhost:8080/v1/graphql' },
      });
      setDataProvider(() => dataProvider);
    };
    buildDataProvider();
  }, []);

  if (!dataProvider) return <p>Loading...</p>;

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

export default App;

How It Works

The data provider converts React Admin queries into the form expected by Hasura's GraphQL API. For example, a React Admin GET_LIST request for a person resource with the parameters:

{
  "pagination": { "page": 1, "perPage": 5 },
  "sort": { "field": "name", "order": "DESC" },
  "filter": {
    "ids": [101, 102]
  }
}

will generate 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
  ) {
    id
    name
    address_id
  }
  total: person_aggregate(
    limit: $limit
    offset: $offset
    order_by: $order_by
    where: $where
  ) {
    aggregate {
      count
    }
  }
}

With the following variables:

{
  "limit": 5,
  "offset": 0,
  "order_by": { "name": "desc" },
  "where": {
    "_and": [
      {
        "id": { "_in": [101, 102] }
      }
    ]
  }
}

React Admin sort and filter objects will be converted appropriately. For example, sorting with dot notation:

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

will generate:

{ "order_by": { "user": { "email": "desc" } } }

and distinct_on:

export const AddressList = () => (
  <List
    sort={{ field: 'city', order: 'DESC' }}
    filter={{ distinct_on: 'city' }}
  >
    ...
  </List>
);

will generate:

{
  "order_by": { "city": "desc" },
  "distinct_on": "city"
}

Keep in mind that distinct_on must be used in conjunction with order_by, otherwise a "distinct_on" columns must match initial "order_by" columns" error will result. See more here.

Options

Customize the Apollo client

You can either supply just the client options:

buildHasuraProvider({
  clientOptions: {
    uri: 'http://localhost:8080/v1/graphql',
    ...otherApolloOptions,
  },
});

or supply the client instance directly:

buildHasuraProvider({ client: myClient });

Adding Authentication Headers

To send authentication headers, supply the client instance directly with headers defined:

import { ApolloClient, InMemoryCache } from '@apollo/client';

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

buildHasuraProvider({ client: myClientWithAuth });

You can also add headers using only client options rather than the client itself:

import { createHttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';

const authLink = setContext((_, { headers }) => ({
  headers: {
    ...headers,
    'x-hasura-admin-secret': 'hasuraAdminSecret',
    // 'Authorization': `Bearer xxxx`,
  },
}));

const httpLink = createHttpLink({
  uri: 'http://localhost:8080/v1/graphql',
});

const clientOptionsWithAuth = {
  link: authLink.concat(httpLink),
};

buildHasuraProvider({ client: clientOptionsWithAuth });

Customize the introspection

These are the default options for introspection:

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),
};

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

Note: exclude and include are mutually exclusive and include will take precedence.

Note: When using functions, the type argument will be a type returned by the introspection query. Refer to the introspection documentation for more information.

Pass the introspection options to the buildHasuraProvider function:

buildHasuraProvider({ introspection: introspectionOptions });

Customize the Data Return

Once the data is returned back from the provider, you can customize it by implementing the DataProvider interface. An example is changing the ID key.

const [dataProvider, setDataProvider] = React.useState<DataProvider | null>(
  null
);

React.useEffect(() => {
  const buildDataProvider = async () => {
    const dataProviderHasura = await buildHasuraProvider({
      clientOptions: {
        uri: 'http://localhost:8080/v1/graphql',
      },
    });
    const modifiedProvider: DataProvider = {
      getList: async (resource, params) => {
        let { data, ...metadata } = await dataProviderHasura.getList(
          resource,
          params
        );

        if (resource === 'example_resource_name') {
          data = data.map(
            (val): Record => ({
              ...val,
              id: val.region_id,
            })
          );
        }

        return { data: data as any[], ...metadata };
      },
      getOne: (resource, params) => dataProviderHasura.getOne(resource, params),
      getMany: (resource, params) =>
        dataProviderHasura.getMany(resource, params),
      getManyReference: (resource, params) =>
        dataProviderHasura.getManyReference(resource, params),
      update: (resource, params) => dataProviderHasura.update(resource, params),
      updateMany: (resource, params) =>
        dataProviderHasura.updateMany(resource, params),
      create: (resource, params) => dataProviderHasura.create(resource, params),
      delete: (resource, params) => dataProviderHasura.delete(resource, params),
      deleteMany: (resource, params) =>
        dataProviderHasura.deleteMany(resource, params),
    };
    setDataProvider(() => modifiedProvider);
  };
  buildDataProvider();
}, []);

Debug Mode

Pass debug: true to log every request to the browser console. Each call is rendered as a collapsible group with its fetchType, params, the printed GraphQL query, variables, response (or error), and duration. Requests are tagged with a sequential id (#1, #2, …) so request and response groups stay correlated even when calls interleave. Schema introspection is also logged the first time it runs.

const dataProvider = await buildHasuraProvider({
  client: apolloClient,
  debug: true,
});

[!WARNING] Debug mode prints everything sent to and received from Hasura, including mutation variables (e.g. password hashes, tokens, or any other sensitive column values). Only enable it in development. Gate it behind an environment check before shipping:

debug: process.env.NODE_ENV === 'development',

Customizing queries

Queries built by this data provider are made up of 3 parts:

  1. The set of fields requested
  2. The variables defining the query constraints like where, order_by, limit, offset
  3. The response format e.g. { data: {...}, total: 100 }

Each of these can be customized — functions overriding numbers 2 and 3 can be passed directly to buildHasuraProvider as shown in Usage, whilst number 1 can be customized in parts using the buildGqlQueryOverrides object argument:

{
  buildFields?: Function,
  buildMetaArgs?: Function,
  buildArgs?: Function,
  buildApolloArgs?: Function,
}

A likely scenario is that you want to override only the buildFields part so that you can customize your GraphQL queries — requesting fewer fields, more fields, nested fields etc.

This can be easily done, and importantly can be done using gql template literal tags, as shown in the examples below. Take a look at this demo application to see it in action.

Example: extending a query to include related entities

By default, the data provider will generate queries that include all fields on a resource, but without any relationships to nested entities. If you would like to keep these base fields but extend the query to also include related entities, then you can write a custom buildFields like this:

import buildDataProvider, { buildFields } from '@aginix/ra-data-hasura';
import type { BuildFields } from '@aginix/ra-data-hasura';
import gql from 'graphql-tag';

const extractFieldsFromQuery = (queryAst) => {
  return queryAst.definitions[0].selectionSet.selections;
};

const EXTENDED_GET_ONE_USER = gql`
  {
    todos_aggregate {
      aggregate {
        count
      }
    }
  }
`;

const customBuildFields: BuildFields = (type, fetchType) => {
  const resourceName = type.name;

  const defaultFields = buildFields(type, fetchType);

  if (resourceName === 'users' && fetchType === 'GET_ONE') {
    const relatedEntities = extractFieldsFromQuery(EXTENDED_GET_ONE_USER);
    defaultFields.push(...relatedEntities);
  }

  return defaultFields;
};

buildDataProvider(options, { buildFields: customBuildFields });

Example: write a completely custom query

If you want full control over the GraphQL query, then you can define the entire set of fields like this:

import gql from 'graphql-tag';
import buildDataProvider, { buildFields } from '@aginix/ra-data-hasura';
import type { BuildFields } from '@aginix/ra-data-hasura';

const extractFieldsFromQuery = (queryAst) => {
  return queryAst.definitions[0].selectionSet.selections;
};

const GET_ONE_USER = gql`
  {
    id
    name
    todos(
      where: { is_completed: { _eq: false } }
      order_by: { created_at: asc }
    ) {
      title
    }
    todos_aggregate {
      aggregate {
        count
      }
    }
  }
`;

const customBuildFields: BuildFields = (type, fetchType) => {
  const resourceName = type.name;

  if (resourceName === 'users' && fetchType === 'GET_ONE') {
    return extractFieldsFromQuery(GET_ONE_USER);
  }

  return buildFields(type, fetchType);
};

buildDataProvider(options, { buildFields: customBuildFields });

Note that when using this approach in particular, it is possible that you will come across this issue.

Special Filter Features

This adapter provides a rich filter syntax using special key patterns. Keys are parsed using:

  • @ as operator separator (e.g. field@_ilike)
  • # as nested field separator (e.g. relation#field)
  • , to create OR conditions across multiple fields

The default comparator is _ilike for strings (automatically wraps value in %value%) and _eq for other types.

Multi-field OR filtering

Comma-separate multiple field paths in a single source to produce an _or condition:

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

Generates:

{
  "where": {
    "_or": [
      { "email": { "_ilike": "%edu%" } },
      { "first_name": { "_eq": "edu" } },
      { "last_name": { "_like": "%edu%" } }
    ]
  }
}

Nested filtering

Use # as a field separator to filter on related object fields:

<TextInput
  label="Search by indication, drug, sponsor, nctid"
  source="indication#name@_ilike,drug#preferred_name@_ilike,sponsor#name@_ilike,trial#nctid@_ilike"
  alwaysOn
/>

Generates:

{
  "where": {
    "_or": [
      { "indication": { "name": { "_ilike": "%TEXT%" } } },
      { "drug": { "name": { "_ilike": "%TEXT%" } } },
      { "sponsor": { "name": { "_ilike": "%TEXT%" } } }
    ]
  }
}

Jsonb filtering

Use @_contains with a #-separated path to filter on JSONB fields:

<TextField label="Theme Color" source="users#preferences@_contains@ux#theme" />

Generates:

{
  "where": {
    "_and": [
      {
        "users": {
          "preferences": {
            "_contains": { "ux": { "theme": "%TEXT" } }
          }
        }
      }
    ]
  }
}

Dynamic JSONB filtering using a related record field:

<FunctionField
  render={(rec) => (
    <ReferenceManyField
      reference="account_plans"
      target={`payments#details@_contains@processor#${rec.processor}_id`}
      source="payment_processor"
    >
      <Datagrid>...</Datagrid>
    </ReferenceManyField>
  )}
/>

Raw Hasura query filter

When the standard filter syntax cannot express your condition, you can pass a raw Hasura where object directly using the hasura-raw-query format. This bypasses all filter processing and injects the value as-is into the where clause.

// In a custom List component or hook:
const filters = {
  status: {
    format: 'hasura-raw-query',
    value: { _in: ['active', 'pending'] },
  },
};

<List filter={filters}>...</List>;

This is especially useful when you need to express conditions that the @ / # syntax does not cover, such as _nin, _similar, or nested _and/_or logic:

const filters = {
  metadata: {
    format: 'hasura-raw-query',
    value: {
      _or: [{ tags: { _contains: 'featured' } }, { priority: { _gte: 5 } }],
    },
  },
};

The key (status, metadata, etc.) is still used as the field path. To inject a condition at the top level of _and, use a key that matches the desired root field.

Programmatic filters (customFilters)

You can pass additional pre-built filter objects via customFilters on the params object. These are merged directly into the _and array alongside the standard filter object:

import { useListController } from 'react-admin';

const MyList = () => {
  const controllerProps = useListController({
    resource: 'posts',
    // customFilters are appended to the _and clause
    filter: { status: 'published' },
    // @ts-ignore — customFilters is not part of the official RA type
    customFilters: [{ author_id: { _eq: currentUserId } }],
  });
  // ...
};

customFilters is an array of raw Hasura filter objects. Each object is added as an additional _and condition alongside any filters derived from the filter param.

Sorting

Sorting by multiple columns

Hasura supports sorting by multiple fields. Since React Admin's List sort prop does not accept arrays, separate multiple fields and orders with commas:

const TodoList = (props) => (
  <List sort={{ field: 'title,is_completed', order: 'asc,desc' }} {...props}>
    <Datagrid rowClick="edit">...</Datagrid>
  </List>
);

generates:

{ "order_by": [{ "title": "asc" }, { "is_completed": "desc" }] }

Fields may contain dots to sort by nested object properties (e.g. user.email).

Null handling in sort

Append @nulls_last or @nulls_first to a sort field to control how NULL values are ordered:

<List sort={{ field: 'published_at@nulls_last', order: 'DESC' }}>...</List>

generates:

{ "order_by": { "published_at": "desc_nulls_last" } }

Supported modifiers: nulls_last, nulls_first.

Disabling pagination

Set perPage to -1 to fetch all records without a limit or offset being sent to Hasura:

<List pagination={false} perPage={-1}>
  ...
</List>

Use with caution on large tables.

Contributing

To modify, extend and test this package locally:

cd ra-data-hasura
npm link

Now use this local package in your React app for testing:

cd my-react-app
npm link @aginix/ra-data-hasura

Build the library by running npm run build — output is generated in the dist folder.

Credits

We would like to thank Steams and all the contributors to this library for porting this adapter to support GraphQL spec, since all the releases till v0.0.8 were based off the REST API spec.