Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GraphQL Simple Data Provider Extensions Support #9403

Closed

Conversation

maxschridde1494
Copy link
Contributor

Data Provider Extensions

A GraphQL backend may support functionality that extends beyond the default Data Provider methods. One such example of this would be implementing GraphQL subscriptions and integrating ra-realtime to power realtime updates in your React-Admin application. The extensions pattern allows you to easily expand the Data Provider methods to power this additional functionality. A Data Provider Extension is defined by the following type:

type DataProviderExtension = {
  methodFactory: (
    dataProvider: DataProvider,
    ...args: any[]
    ) => { [k: string]: DataProviderMethod };
  factoryArgs?: any[];
  introspectionOperationNames?: IntrospectionOptions['operationNames'];
}

The methodFactory is a required function attribute that generates the additional Data Provider methods. It always receives the dataProvider as it's first argument. Arguments defined in the factoryArgs optional attribute will also be passed into methodFactory. introspectionOperationNames is an optional object attribute that allows you to inform React-Admin hooks and UI components of how these methods map to the GraphQL schema.

Realtime Extension

I've added an example Realtime Data Provider Extension that can be opted into. If your app uses ra-realtime, you can drop in the Realtime Extension and light up realtime events. Here is an example integration:

// in App.js
import React from 'react';
import { Component } from 'react';
import buildGraphQLProvider, { defaultOptions, DataProviderExtensions } from 'ra-data-graphql-simple';
import { Admin, Resource } from 'react-admin';

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

const dPOptions = {
    clientOptions: { uri: 'http://localhost:4000' },
    extensions: [DataProviderExtensions.Realtime]
}

const App = () => {

    const [dataProvider, setDataProvider] = React.useState(null);
    React.useEffect(() => {
        buildGraphQLProvider(dPOptions)
            .then(graphQlDataProvider => setDataProvider(() => graphQlDataProvider));
    }, []);

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

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

export default App;

@slax57
Copy link
Contributor

slax57 commented Nov 9, 2023

Hi,
Thank you so much for submitting this contribution.
It's gonna take us some time to review, as it's a quite substantial PR, but we are definitely willing to have a look.
Thank you for your patience.

Copy link
Collaborator

@djhi djhi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really cool. I wonder whether the extension code should be in ra-data-graphql directly

packages/ra-data-graphql-simple/README.md Outdated Show resolved Hide resolved
packages/ra-data-graphql/src/index.ts Outdated Show resolved Hide resolved
@maxschridde1494
Copy link
Contributor Author

maxschridde1494 commented Nov 27, 2023

@djhi That's a great point. I think moving it down a level would be a good idea. That was definitely on my wishlist when integrating with ra-data-graphql-simple dp--a more accessible and customizable interface with ra-data-graphql. Plus, the extensions pattern would then be accessible to other ra-data-graphql wrappers. Should I close this PR and re-open against ra-data-graphql?

Copy link
Collaborator

@djhi djhi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please explain what is currently preventing you from implementing such custom methods? Is it because we use a Proxy? Because we don't allow you to specify the DataProvider type? Something else?

@maxschridde1494
Copy link
Contributor Author

All of our ra-data-graphql-simple enhancements are feature driven. Some of these features include

  • single request bulk actions
  • live dashboard updates
  • default client accessibility for non-REST requests
  • default client accessibility to open up more of the apollo-client api (currently restricted to query & mutation).

As mentioned in #9392 (comment), ra-data-graphql-simple was a great starting point for us, but did not handle some of these features out of the box. Initially, we forked and then extended. After we saw how little we needed to change, and thinking about how common these use cases are, it seemed sensible to come up with a pattern that would allow for easy extensibility without the requirement of building a new provider.

# Conflicts:
#	packages/ra-data-graphql-simple/src/index.ts
#	packages/ra-data-graphql/src/index.ts
@maxschridde1494
Copy link
Contributor Author

@djhi Sorry I missed your change requests. Just got those pushed up. Do you have any final thoughts on this proposal?

@djhi
Copy link
Collaborator

djhi commented Jan 5, 2024

After careful consideration, we believe this abstraction layer is unnecessary. You can already add those methods by extending the resulting dataProvider:

const dataProvider = await buildGraphQLProvider({ clientOptions: { uri: 'http://localhost:4000' } });
return {
    ...dataProvider,
    subscribe: async (topic: string, subscriptionCallback: any) => {
        // The code you added for it
    },
    // etc
};

Besides, should you need to add custom queries generated from the schema, you would need to provide your own buildQuery implementation anyway. The issue is that you don't have access to the introspectionResults nor to the new internal callApollo at this point:

import { DataProvider } from 'ra-core';
import { BuildQueryFactory } from 'ra-data-graphql';
import buildDataProvider, { buildQuery } from 'ra-data-graphql-simple';

interface CustomDataProvider extends DataProvider {
    subscribe: (resource: string, params: any) => void;
}

export const buildCustomDataProvider: Promise<CustomDataProvider> = async (
    options: Parameters<typeof buildDataProvider>[0]
) => {
    const dataProvider = await buildDataProvider({
        ...options,
        buildQuery: buildCustomQuery,
    });

    return {
        ...dataProvider,
        subscribe: (resource, params) => {
            // takes introspectionResults but we don't have access to it here
            buildCustomQuery()
        },
    };
};

const buildCustomQuery: BuildQueryFactory = introspectionResults => (
    name: string,
    resource: string,
    params: any
) => {
    if (name === 'subscribe') {
        // Build the subscription query and return it
        return null;
    }

    return buildQuery(introspectionResults)(name, resource, params);
};

I believe the solution is to add a getIntrospection method to the returned dataProvider.

import { DataProvider } from 'ra-core';
import { BuildQueryFactory } from 'ra-data-graphql';
import buildDataProvider, { buildQuery } from 'ra-data-graphql-simple';

interface CustomDataProvider extends DataProvider {
    subscribe: (resource: string, params: any) => void;
}

export const buildCustomDataProvider: Promise<CustomDataProvider> = async (
    options: Parameters<typeof buildDataProvider>[0]
) => {
    const dataProvider = await buildDataProvider({
        ...options,
        buildQuery: buildCustomQuery,
    });

    return {
        ...dataProvider,
        subscribe: async (resource, params) => {
            const introspection = await dataProvider.getIntrospection();

            // Do what you want with it, including calling your buildCustomQuery but not necessarily.
        },
    };
};

const buildCustomQuery: BuildQueryFactory = introspectionResults => (
    name: string,
    resource: string,
    params: any
) => {
    if (name === 'subscribe') {
        // Build the subscription query and return it
        return null;
    }

    return buildQuery(introspectionResults)(name, resource, params);
};

@fzaninotto fzaninotto closed this Jan 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants