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

[ra-data-graphql] Cached GraphQL Introspection breaks multi-DataProvider apps #8736

Closed
elstgav opened this issue Mar 16, 2023 · 2 comments · Fixed by #8740
Closed

[ra-data-graphql] Cached GraphQL Introspection breaks multi-DataProvider apps #8736

elstgav opened this issue Mar 16, 2023 · 2 comments · Fixed by #8740
Labels

Comments

@elstgav
Copy link
Contributor

elstgav commented Mar 16, 2023

What you were expecting:

Apps with multiple graphql data providers (e.g. combined with combineDataProviders) would be able to query different GraphQL servers without error.

What happened instead:

The first graphql dataprovider that runs caches its introspection query results globally. Other dataproviders check that same introspection query and fail.

Steps to reproduce:

Imagine the following setup: an app that combines two data providers, for API A and B:

import { combineDataProviders } from 'react-admin'
import buildGraphQLProvider from 'ra-data-graphql-simple'

export const App = () => {
  const clientA = new ApolloClient({ uri: '/api-A' })
  const clientB = new ApolloClient({ uri: '/api-B' })

  const [providerA, setProviderA] = useState<DataProvider>()
  const [providerB, setProviderB] = useState<DataProvider>()

  useEffect(() => {
    buildGraphQLProvider({ client: clientA }).then((provider) => setProviderA(provider))
  }, [clientA])

  useEffect(() => {
    buildGraphQLProvider({ client: clientB }).then((provider) => setProviderB(provider))
  }, [clientB])

  if (!providerA || !providerB) return <div>Loading...</div>

  const dataProvider = combineDataProviders((resource) => {
    switch (resource) {
      case 'resourceA':  return providerA
      case 'resourceB':  return providerB
      default: throw new Error(`No data provider for ${resource}`)
    }
  })

  return (
    <Admin dataProvider={dataProvider}>
      <Resource name='resourceA' list={ResourceAList} />
      <Resource name='resourceB' list={ResourceBList} />
    </Admin>
  )
}
  1. Navigate to /resourceA
    a. Behind the scenes, ra-data-graphql runs an introspection query for A’s graphql server.
    b. resourceA is queried and presented.
  2. Navigate to /resourceB
    a. Behind the scenes, ra-data-graphql uses the cached introspection query for A, even though we need introspection for B.

Result:

resourceB is not queried, and instead an error is thrown:

Error: Unknown resource resourceB. Make sure it has been declared on your server side schema. 
Known resources are resourceA

Other information:

I believe the source of the problem is that ra-data-graphql/src/introspection.ts is built as a singleton, caching the introspection query promise with a let variable within the node module, regardless of different client or options arguments:

export const introspectSchema = async (
client: ApolloClient<unknown>,
options: IntrospectionOptions
) => {
if (introspectionPromise) {
return introspectionPromise;
}
introspectionPromise = runSchemaIntrospection(client, options);
return introspectionPromise;
};

A potential solution is for introspection.ts to memoize the promise depending on client and options.

Environment

  • React-admin version: 4.8.3
  • Last version that did not exhibit the issue (if applicable): N/A
  • React version: 17.0.2
  • Browser: Chrome 111.0.5563.64
  • Stack trace (in case of a JS error):
Stack Trace
consoleObservable.ts:34 Error: Unknown resource resourceB. Make sure it has been declared on
your server side schema. Known resources are resourceA
    at index.js:16:1
    at index.ts:170:1
    at step (constants.ts:21:1)
    at Object.next (constants.ts:21:1)
    at fulfilled (constants.ts:21:1)
console.<computed> @ consoleObservable.ts:34
overrideMethod @ react_devtools_backend.js:2655
onError @ query.js:358
reject @ retryer.js:67
(anonymous) @ retryer.js:132
Promise.catch (async)
run @ retryer.js:116
Retryer @ retryer.js:156
fetch @ query.js:332
executeFetch @ queryObserver.js:199
onSubscribe @ queryObserver.js:40
subscribe @ subscribable.js:16
(anonymous) @ useBaseQuery.js:60
invokePassiveEffectCreate @ react-dom.development.js:23487
callCallback @ react-dom.development.js:3945
invokeGuardedCallbackDev @ react-dom.development.js:3994
invokeGuardedCallback @ react-dom.development.js:4056
flushPassiveEffectsImpl @ react-dom.development.js:23574
unstable_runWithPriority @ scheduler.development.js:468
runWithPriority$1 @ react-dom.development.js:11276
flushPassiveEffects @ react-dom.development.js:23447
performSyncWorkOnRoot @ react-dom.development.js:22269
(anonymous) @ react-dom.development.js:11327
unstable_runWithPriority @ scheduler.development.js:468
runWithPriority$1 @ react-dom.development.js:11276
flushSyncCallbackQueueImpl @ react-dom.development.js:11322
flushSyncCallbackQueue @ react-dom.development.js:11309
discreteUpdates$1 @ react-dom.development.js:22420
discreteUpdates @ react-dom.development.js:3756
dispatchDiscreteEvent @ react-dom.development.js:5889
@elstgav elstgav changed the title Cached GraphQL Introspection breaks multi-DataProvider apps [ra-data-graphql] Cached GraphQL Introspection breaks multi-DataProvider apps Mar 16, 2023
@slax57
Copy link
Contributor

slax57 commented Mar 17, 2023

Thanks for this detailed report and investigation work!

@elstgav
Copy link
Contributor Author

elstgav commented Mar 17, 2023

Thanks for the quick fix!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants