diff --git a/.changeset/nine-elephants-retire.md b/.changeset/nine-elephants-retire.md new file mode 100644 index 00000000000..cca48d20cee --- /dev/null +++ b/.changeset/nine-elephants-retire.md @@ -0,0 +1,5 @@ +--- +'@graphiql/react': minor +--- + +Allow passing introspection data to the `schema` prop of the `SchemaContextProvider` component diff --git a/.changeset/nine-eyes-relax.md b/.changeset/nine-eyes-relax.md new file mode 100644 index 00000000000..5725859db2b --- /dev/null +++ b/.changeset/nine-eyes-relax.md @@ -0,0 +1,5 @@ +--- +'graphiql': minor +--- + +Allow passing introspection data to the `schema` prop of the `GraphiQL` component diff --git a/.changeset/serious-ties-compete.md b/.changeset/serious-ties-compete.md new file mode 100644 index 00000000000..9103e55d775 --- /dev/null +++ b/.changeset/serious-ties-compete.md @@ -0,0 +1,5 @@ +--- +'@graphiql/react': patch +--- + +Set the schema correctly after refetching introspection (e.g. when the `fetcher` prop changes) diff --git a/packages/graphiql-react/src/schema.tsx b/packages/graphiql-react/src/schema.tsx index 5d829594ce3..af5a0309e20 100644 --- a/packages/graphiql-react/src/schema.tsx +++ b/packages/graphiql-react/src/schema.tsx @@ -12,6 +12,7 @@ import { GraphQLError, GraphQLSchema, IntrospectionQuery, + isSchema, validateSchema, } from 'graphql'; import { @@ -55,7 +56,7 @@ type SchemaContextProviderProps = { dangerouslyAssumeSchemaIsValid?: boolean; fetcher: Fetcher; onSchemaChange?(schema: GraphQLSchema): void; - schema?: GraphQLSchema | null; + schema?: GraphQLSchema | IntrospectionQuery | null; } & IntrospectionArgs; export function SchemaContextProvider(props: SchemaContextProviderProps) { @@ -63,9 +64,7 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { nonNull: true, caller: SchemaContextProvider, }); - const [schema, setSchema] = useState( - props.schema || null, - ); + const [schema, setSchema] = useState(); const [isFetching, setIsFetching] = useState(false); const [fetchError, setFetchError] = useState(null); @@ -73,7 +72,13 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { * Synchronize prop changes with state */ useEffect(() => { - setSchema(props.schema); + setSchema( + isSchema(props.schema) || + props.schema === null || + props.schema === undefined + ? props.schema + : undefined, + ); }, [props.schema]); /** @@ -104,46 +109,59 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { */ const { fetcher, onSchemaChange } = props; useEffect(() => { - // Only introspect if there is no schema provided via props - if (props.schema !== undefined) { + /** + * Only introspect if there is no schema provided via props. If the + * prop is passed an introspection result, we do continue but skip the + * introspection request. + */ + if (isSchema(props.schema) || props.schema === null) { return; } let isActive = true; - - const parsedHeaders = parseHeaderString(headersRef.current); - if (!parsedHeaders.isValidJSON) { - setFetchError('Introspection failed as headers are invalid.'); - return; - } - - const fetcherOpts: FetcherOpts = parsedHeaders.headers - ? { headers: parsedHeaders.headers } - : {}; - - const fetch = fetcherReturnToPromise( - fetcher( - { - query: introspectionQuery, - operationName: introspectionQueryName, - }, - fetcherOpts, - ), - ); - - if (!isPromise(fetch)) { - setFetchError('Fetcher did not return a Promise for introspection.'); - return; - } - - setIsFetching(true); - - fetch - .then(result => { - if (typeof result === 'object' && result !== null && 'data' in result) { - return result; - } - + setSchema(undefined); + + const maybeIntrospectionData = props.schema; + async function introspect() { + if (maybeIntrospectionData) { + // No need to introspect if we already have the data + return maybeIntrospectionData; + } + + const parsedHeaders = parseHeaderString(headersRef.current); + if (!parsedHeaders.isValidJSON) { + setFetchError('Introspection failed as headers are invalid.'); + return; + } + + const fetcherOpts: FetcherOpts = parsedHeaders.headers + ? { headers: parsedHeaders.headers } + : {}; + + const fetch = fetcherReturnToPromise( + fetcher( + { + query: introspectionQuery, + operationName: introspectionQueryName, + }, + fetcherOpts, + ), + ); + + if (!isPromise(fetch)) { + setFetchError('Fetcher did not return a Promise for introspection.'); + return; + } + + setIsFetching(true); + + let result = await fetch; + + if ( + typeof result !== 'object' || + result === null || + !('data' in result) + ) { // Try the stock introspection query first, falling back on the // sans-subscriptions query for services which do not yet support it. const fetch2 = fetcherReturnToPromise( @@ -160,41 +178,44 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { 'Fetcher did not return a Promise for introspection.', ); } - return fetch2; - }) - .then(result => { + result = await fetch2; + } + + setIsFetching(false); + + if (result?.data && '__schema' in result.data) { + return result.data as IntrospectionQuery; + } + + // handle as if it were an error if the fetcher response is not a string or response.data is not present + const responseString = + typeof result === 'string' ? result : formatResult(result); + setFetchError(responseString); + } + + introspect() + .then(introspectionData => { // Don't continue if the effect has already been cleaned up - if (!isActive) { + if (!isActive || !introspectionData) { return; } - if (result?.data && '__schema' in result.data) { - try { - const newSchema = buildClientSchema( - result.data as IntrospectionQuery, - ); - // Only override the schema in state if it's still `undefined` (the - // prop and thus the state could have changed while introspecting, - // so this avoids a race condition by prioritizing the state that - // was set after the introspection request was initialized) - setSchema(current => { - if (current === undefined) { - onSchemaChange?.(newSchema); - return newSchema; - } - return current; - }); - } catch (error) { - setFetchError(formatError(error as Error)); - } - } else { - // handle as if it were an error if the fetcher response is not a string or response.data is not present - const responseString = - typeof result === 'string' ? result : formatResult(result); - setFetchError(responseString); + try { + const newSchema = buildClientSchema(introspectionData); + // Only override the schema in state if it's still `undefined` (the + // prop and thus the state could have changed while introspecting, + // so this avoids a race condition by prioritizing the state that + // was set after the introspection request was initialized) + setSchema(current => { + if (current === undefined) { + onSchemaChange?.(newSchema); + return newSchema; + } + return current; + }); + } catch (error) { + setFetchError(formatError(error as Error)); } - - setIsFetching(false); }) .catch(error => { // Don't continue if the effect has already been cleaned up diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 1433de435bd..a18d945241f 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -18,6 +18,7 @@ import { ValidationRule, FragmentDefinitionNode, DocumentNode, + IntrospectionQuery, } from 'graphql'; import { @@ -106,9 +107,11 @@ export type GraphiQLProps = { */ fetcher: Fetcher; /** - * Optionally provide the `GraphQLSchema`. If present, GraphiQL skips schema introspection. + * Optionally provide the `GraphQLSchema`. If present, GraphiQL skips schema + * introspection. This prop also accepts the result of an introspection query + * which will be used to create a `GraphQLSchema` */ - schema?: GraphQLSchema | null; + schema?: GraphQLSchema | IntrospectionQuery | null; /** * An array of graphql ValidationRules */