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

feat(graphiql): pass introspection data to schema prop #2574

Merged
merged 2 commits into from
Jul 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nine-elephants-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphiql/react': minor
---

Allow passing introspection data to the `schema` prop of the `SchemaContextProvider` component
5 changes: 5 additions & 0 deletions .changeset/nine-eyes-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'graphiql': minor
---

Allow passing introspection data to the `schema` prop of the `GraphiQL` component
5 changes: 5 additions & 0 deletions .changeset/serious-ties-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphiql/react': patch
---

Set the schema correctly after refetching introspection (e.g. when the `fetcher` prop changes)
163 changes: 92 additions & 71 deletions packages/graphiql-react/src/schema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
GraphQLError,
GraphQLSchema,
IntrospectionQuery,
isSchema,
validateSchema,
} from 'graphql';
import {
Expand Down Expand Up @@ -55,25 +56,29 @@ type SchemaContextProviderProps = {
dangerouslyAssumeSchemaIsValid?: boolean;
fetcher: Fetcher;
onSchemaChange?(schema: GraphQLSchema): void;
schema?: GraphQLSchema | null;
schema?: GraphQLSchema | IntrospectionQuery | null;
} & IntrospectionArgs;

export function SchemaContextProvider(props: SchemaContextProviderProps) {
const { initialHeaders, headerEditor } = useEditorContext({
nonNull: true,
caller: SchemaContextProvider,
});
const [schema, setSchema] = useState<MaybeGraphQLSchema>(
props.schema || null,
);
const [schema, setSchema] = useState<MaybeGraphQLSchema>();
const [isFetching, setIsFetching] = useState(false);
const [fetchError, setFetchError] = useState<string | null>(null);

/**
* Synchronize prop changes with state
*/
useEffect(() => {
setSchema(props.schema);
setSchema(
isSchema(props.schema) ||
props.schema === null ||
props.schema === undefined
? props.schema
: undefined,
);
}, [props.schema]);

/**
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions packages/graphiql/src/components/GraphiQL.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ValidationRule,
FragmentDefinitionNode,
DocumentNode,
IntrospectionQuery,
} from 'graphql';

import {
Expand Down Expand Up @@ -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
*/
Expand Down