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

manually re-trigger introspection #2618

Merged
merged 1 commit into from
Aug 4, 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/shy-parents-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'graphiql': minor
---

Add a toolbar button for manually triggering introspection
5 changes: 5 additions & 0 deletions .changeset/wild-rings-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphiql/react': minor
---

Add a method `introspect` to the schema context and provide a short key (`Shift-Ctrl-R`) for triggering introspection
82 changes: 56 additions & 26 deletions packages/graphiql-react/src/schema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
Dispatch,
ReactNode,
SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
Expand All @@ -32,14 +33,15 @@ import { createContextHook, createNullableContext } from './utility/context';
* There's a semantic difference between `null` and `undefined`:
* - When `null` is passed explicitly as prop, GraphiQL will run schema-less
* (i.e. it will never attempt to fetch the schema, even when calling the
* `useFetchSchema` hook).
* `introspect` function).
* - When `schema` is `undefined` GraphiQL will attempt to fetch the schema
* when calling `useFetchSchema`.
* when calling `introspect`.
*/
type MaybeGraphQLSchema = GraphQLSchema | null | undefined;

export type SchemaContextType = {
fetchError: string | null;
introspect(): void;
isFetching: boolean;
schema: MaybeGraphQLSchema;
setFetchError: Dispatch<SetStateAction<string | null>>;
Expand Down Expand Up @@ -68,6 +70,12 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) {
const [isFetching, setIsFetching] = useState(false);
const [fetchError, setFetchError] = useState<string | null>(null);

/**
* A counter that is incremented each time introspection is triggered or the
* schema state is updated.
*/
const counterRef = useRef(0);

/**
* Synchronize prop changes with state
*/
Expand All @@ -79,6 +87,12 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) {
? props.schema
: undefined,
);

/**
* Increment the counter so that in-flight introspection requests don't
* override this change.
*/
counterRef.current++;
}, [props.schema]);

/**
Expand Down Expand Up @@ -108,7 +122,7 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) {
* Fetch the schema
*/
const { fetcher, onSchemaChange } = props;
useEffect(() => {
const introspect = useCallback(() => {
/**
* Only introspect if there is no schema provided via props. If the
* prop is passed an introspection result, we do continue but skip the
Expand All @@ -118,11 +132,12 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) {
return;
}

let isActive = true;
const counter = ++counterRef.current;

setSchema(undefined);

const maybeIntrospectionData = props.schema;
async function introspect() {
async function fetchIntrospectionData() {
if (maybeIntrospectionData) {
// No need to introspect if we already have the data
return maybeIntrospectionData;
Expand Down Expand Up @@ -193,43 +208,37 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) {
setFetchError(responseString);
}

introspect()
fetchIntrospectionData()
.then(introspectionData => {
// Don't continue if the effect has already been cleaned up
if (!isActive || !introspectionData) {
console.log(counter, counterRef.current);
/**
* Don't continue if another introspection request has been started in
* the meantime or if there is no introspection data.
*/
if (counter !== counterRef.current || !introspectionData) {
return;
}

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;
});
setSchema(newSchema);
onSchemaChange?.(newSchema);
} catch (error) {
setFetchError(formatError(error as Error));
}
})
.catch(error => {
// Don't continue if the effect has already been cleaned up
if (!isActive) {
/**
* Don't continue if another introspection request has been started in
* the meantime.
*/
if (counter !== counterRef.current) {
return;
}

setFetchError(formatError(error));
setIsFetching(false);
});

return () => {
isActive = false;
};
}, [
fetcher,
introspectionQueryName,
Expand All @@ -239,6 +248,26 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) {
props.schema,
]);

/**
* Trigger introspection automatically
*/
useEffect(() => {
introspect();
}, [introspect]);

/**
* Trigger introspection manually via short key
*/
useEffect(() => {
function triggerIntrospection(event: KeyboardEvent) {
if (event.keyCode === 82 && event.shiftKey && event.ctrlKey) {
introspect();
}
}
window.addEventListener('keydown', triggerIntrospection);
return () => window.removeEventListener('keydown', triggerIntrospection);
});

/**
* Derive validation errors from the schema
*/
Expand All @@ -256,13 +285,14 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) {
const value = useMemo(
() => ({
fetchError,
introspect,
isFetching,
schema,
setFetchError,
setSchema,
validationErrors,
}),
[fetchError, isFetching, schema, validationErrors],
[fetchError, introspect, isFetching, schema, validationErrors],
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function TypeDocWithContext(props: { type: GraphQLNamedType }) {
<SchemaContext.Provider
value={{
fetchError: null,
introspect() {},
isFetching: false,
schema: ExampleSchema,
setFetchError() {},
Expand Down
5 changes: 5 additions & 0 deletions packages/graphiql/src/components/GraphiQL.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,11 @@ class GraphiQLWithContext extends React.Component<
}
label="History"
/>
<ToolbarButton
onClick={() => this.props.schemaContext.introspect()}
title="Fetch GraphQL schema using introspection (Shift-Ctrl-R)"
label="Introspect"
/>
{this.props.toolbar?.additionalContent
? this.props.toolbar.additionalContent
: null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ExampleSchema } from './ExampleSchema';

const defaultSchemaContext: SchemaContextType = {
fetchError: null,
introspect() {},
isFetching: false,
schema: ExampleSchema,
setFetchError() {},
Expand Down
15 changes: 7 additions & 8 deletions packages/vscode-graphql-syntax/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
# vscode-graphql-syntax

## 1.0.4
### Patch Changes


### Patch Changes

- [#2573](https://github.com/graphql/graphiql/pull/2573) [`a358ac1d`](https://github.com/graphql/graphiql/commit/a358ac1d00082643e124085bca09992adeef212a) Thanks [@acao](https://github.com/acao)! - ## Enhancement

Here we move vscode grammars and basic language support to a new [`GraphQL.vscode-graphql-syntax`](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql-syntax) extension. `GraphQL.vscode-graphql` now depends on this new syntax extension. This constitutes no breaking change for `vscode-graphql` users, as this extension will be installed automatically as an `extensionDependency` for `vscode-graphql`. Both extensions will now have independent release lifecycles, but vscode will keep them both up to date for you :)

Firstly, this allows users to only install the syntax highlighting extension if they don't need LSP server features.

Secondly, this subtle but important change allows alternative LSP servers and non-LSP graphql extensions to use (and contribute!) to our shared, graphql community syntax highlighting. In some ways, it acts as a shared tooling & annotation spec, though it is intended just for vscode, it perhaps can be used as a point of reference for others implementing (embedded) graphql syntax highlighting elsewhere!

If your language and/or library and/or framework would like vscode highlighting, come [join the party](https://github.com/graphql/graphiql/tree/main/packages/vscode-graphql-syntax#contributing)!

If you use relay, we would highly reccomend using the `relay-compiler lsp` extension for vscode [Relay Graphql](https://marketplace.visualstudio.com/items?itemName=meta.relay) (`meta.relay`). They will be [using the new standalone syntax extension](https://github.com/facebook/relay/pull/4032) very soon!

Even non-relay users may want to try this extension as an alternative to our reference implementation, as relay's configuration has relative similarity with `graphql-config`'s format, and doesn't necessitate the use of relay client afaik. We are working hard to optimize and improve `graphql-language-service-server` as a typescript reference implementation, and have some exciting features coming soon, however it's hard to offer more than a brand new & highly performant graphql LSP server written in Rust based on the latest graphql spec with a (mostly) paid team and dedicated open source ecosystem community of co-maintainers! And their implementation appears to allow you to opt out of any relay-specific conventions if you need more flexibility.