Skip to content

Commit

Permalink
feat: add external fragment execution to GraphiQL
Browse files Browse the repository at this point in the history
  • Loading branch information
acao committed Jan 5, 2021
1 parent ab65f53 commit b9f3d7e
Show file tree
Hide file tree
Showing 30 changed files with 271 additions and 108 deletions.
1 change: 1 addition & 0 deletions packages/codemirror-graphql/src/__tests__/hint-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ function createEditorWithHint() {
schema: TestSchema,
closeOnUnfocus: false,
completeSingle: false,
externalFragments: [],
},
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@

import { useMemo } from 'react';

import getQueryFacts from '../../utility/getQueryFacts';
import getOperationFacts from '../../utility/getQueryFacts';
import useSchema from './useSchema';
import useOperation from './useOperation';

export default function useQueryFacts() {
const schema = useSchema();
const { text } = useOperation();
return useMemo(() => (schema ? getQueryFacts(schema, text) : null), [
return useMemo(() => (schema ? getOperationFacts(schema, text) : null), [
schema,
text,
]);
Expand Down
7 changes: 4 additions & 3 deletions packages/graphiql-2-rfc-context/src/utility/getQueryFacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
OperationDefinitionNode,
NamedTypeNode,
GraphQLNamedType,
Kind,
} from 'graphql';

export type VariableToType = {
Expand All @@ -30,7 +31,7 @@ export type QueryFacts = {
*
* If the query cannot be parsed, returns undefined.
*/
export default function getQueryFacts(
export default function getOperationFacts(
schema?: GraphQLSchema,
documentStr?: string | null,
): QueryFacts | undefined {
Expand All @@ -52,7 +53,7 @@ export default function getQueryFacts(
// Collect operations by their names.
const operations: OperationDefinitionNode[] = [];
documentAST.definitions.forEach(def => {
if (def.kind === 'OperationDefinition') {
if (def.kind === Kind.OPERATION_DEFINITION) {
operations.push(def);
}
});
Expand All @@ -71,7 +72,7 @@ export function collectVariables(
[variable: string]: GraphQLNamedType;
} = Object.create(null);
documentAST.definitions.forEach(definition => {
if (definition.kind === 'OperationDefinition') {
if (definition.kind === Kind.OPERATION_DEFINITION) {
const variableDefinitions = definition.variableDefinitions;
if (variableDefinitions) {
variableDefinitions.forEach(({ variable, type }) => {
Expand Down
1 change: 1 addition & 0 deletions packages/graphiql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"codemirror": "^5.54.0",
"codemirror-graphql": "^0.14.0",
"copy-to-clipboard": "^3.2.0",
"graphql-language-service": "^3.0.2",
"entities": "^2.0.0",
"markdown-it": "^10.0.0"
},
Expand Down
63 changes: 53 additions & 10 deletions packages/graphiql/src/components/GraphiQL.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ import {
GraphQLSchema,
parse,
print,
visit,
OperationDefinitionNode,
IntrospectionQuery,
GraphQLType,
ValidationRule,
FragmentDefinitionNode,
DocumentNode,
} from 'graphql';
import copyToClipboard from 'copy-to-clipboard';
import { getFragmentDependenciesForAST } from 'graphql-language-service';

import { ExecuteButton } from './ExecuteButton';
import { ImagePreview } from './ImagePreview';
Expand All @@ -38,7 +41,7 @@ import { DocExplorer } from './DocExplorer';
import { QueryHistory } from './QueryHistory';
import CodeMirrorSizer from '../utility/CodeMirrorSizer';
import StorageAPI, { Storage } from '../utility/StorageAPI';
import getQueryFacts, { VariableToType } from '../utility/getQueryFacts';
import getOperationFacts, { VariableToType } from '../utility/getQueryFacts';
import getSelectedOperationName from '../utility/getSelectedOperationName';
import debounce from '../utility/debounce';
import find from '../utility/find';
Expand Down Expand Up @@ -80,6 +83,7 @@ export type FetcherParams = {
export type FetcherOpts = {
headers?: { [key: string]: any };
shouldPersistHeaders: boolean;
documentAST?: DocumentNode;
};

export type FetcherResult =
Expand Down Expand Up @@ -125,7 +129,7 @@ export type GraphiQLProps = {
shouldPersistHeaders?: boolean;
externalFragments?: string | FragmentDefinitionNode[];
onCopyQuery?: (query?: string) => void;
onEditQuery?: (query?: string) => void;
onEditQuery?: (query?: string, documentAST?: DocumentNode) => void;
onEditVariables?: (value: string) => void;
onEditHeaders?: (value: string) => void;
onEditOperationName?: (operationName: string) => void;
Expand Down Expand Up @@ -160,6 +164,7 @@ export type GraphiQLState = {
subscription?: Unsubscribable | null;
variableToType?: VariableToType;
operations?: OperationDefinitionNode[];
documentAST?: DocumentNode;
};

/**
Expand Down Expand Up @@ -227,8 +232,7 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
: defaultQuery;

// Get the initial query facts.
const queryFacts = getQueryFacts(props.schema, query);

const queryFacts = getOperationFacts(props.schema, query);
// Determine the initial variables to display.
const variables =
props.variables !== undefined
Expand Down Expand Up @@ -814,6 +818,7 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {

const fetcherOpts: FetcherOpts = {
shouldPersistHeaders: Boolean(this.props.shouldPersistHeaders),
documentAST: this.state.documentAST,
};
if (this.state.headers && this.state.headers.trim().length > 2) {
fetcherOpts.headers = JSON.parse(this.state.headers);
Expand Down Expand Up @@ -873,7 +878,7 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {

if (typeof result !== 'string' && 'data' in result) {
const schema = buildClientSchema(result.data);
const queryFacts = getQueryFacts(schema, this.state.query);
const queryFacts = getOperationFacts(schema, this.state.query);
this.safeSetState({ schema, ...queryFacts });
} else {
const responseString =
Expand Down Expand Up @@ -926,14 +931,50 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
if (typeof jsonHeaders !== 'object') {
throw new Error('Headers are not a JSON object.');
}
// TODO: memoize this
if (this.props.externalFragments) {
const externalFragments = new Map<string, FragmentDefinitionNode>();

if (Array.isArray(this.props.externalFragments)) {
this.props.externalFragments.forEach(def => {
externalFragments.set(def.name.value, def);
});
} else {
visit(
parse(this.props.externalFragments, {
experimentalFragmentVariables: true,
}),
{
FragmentDefinition(def) {
externalFragments.set(def.name.value, def);
},
},
);
}
const fragmentDependencies = getFragmentDependenciesForAST(
this.state.documentAST!,
externalFragments,
);
if (fragmentDependencies.length > 0) {
query +=
'\n' +
fragmentDependencies
.map((node: FragmentDefinitionNode) => print(node))
.join('\n');
}
}

const fetch = fetcher(
{
query,
variables: jsonVariables,
operationName,
},
{ headers: jsonHeaders, shouldPersistHeaders },
{
headers: jsonHeaders,
shouldPersistHeaders,
documentAST: this.state.documentAST,
},
);

if (isPromise(fetch)) {
Expand Down Expand Up @@ -1117,7 +1158,9 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
handlePrettifyQuery = () => {
const editor = this.getQueryEditor();
const editorContent = editor?.getValue() ?? '';
const prettifiedEditorContent = print(parse(editorContent));
const prettifiedEditorContent = print(
parse(editorContent, { experimentalFragmentVariables: true }),
);

if (prettifiedEditorContent !== editorContent) {
editor?.setValue(prettifiedEditorContent);
Expand Down Expand Up @@ -1164,7 +1207,7 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
return;
}

const ast = parse(query);
const ast = this.state.documentAST!;
editor.setValue(print(mergeAST(ast, this.state.schema)));
};

Expand All @@ -1181,7 +1224,7 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
});
this._storage.set('query', value);
if (this.props.onEditQuery) {
return this.props.onEditQuery(value);
return this.props.onEditQuery(value, queryFacts?.documentAST);
}
});

Expand All @@ -1206,7 +1249,7 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
prevOperations?: OperationDefinitionNode[],
schema?: GraphQLSchema,
) => {
const queryFacts = getQueryFacts(schema, query);
const queryFacts = getOperationFacts(schema, query);
if (queryFacts) {
// Update operation name should any query names change.
const updatedOperationName = getSelectedOperationName(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
parse,
} from 'graphql';

import { collectVariables } from '../getQueryFacts';
import { collectVariables } from '../getOperationFacts';

describe('collectVariables', () => {
const TestType = new GraphQLObjectType({
Expand Down
26 changes: 18 additions & 8 deletions packages/graphiql/src/utility/getQueryFacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
OperationDefinitionNode,
NamedTypeNode,
GraphQLNamedType,
visit,
} from 'graphql';

export type VariableToType = {
Expand All @@ -22,15 +23,16 @@ export type VariableToType = {
export type QueryFacts = {
variableToType?: VariableToType;
operations?: OperationDefinitionNode[];
documentAST?: DocumentNode;
};

/**
* Provided previous "queryFacts", a GraphQL schema, and a query document
* Provided previous "operationFacts", a GraphQL schema, and a query document
* string, return a set of facts about that query useful for GraphiQL features.
*
* If the query cannot be parsed, returns undefined.
*/
export default function getQueryFacts(
export default function getOperationFacts(
schema?: GraphQLSchema,
documentStr?: string | null,
): QueryFacts | undefined {
Expand All @@ -40,7 +42,9 @@ export default function getQueryFacts(

let documentAST: DocumentNode;
try {
documentAST = parse(documentStr);
documentAST = parse(documentStr, {
experimentalFragmentVariables: true,
});
} catch {
return;
}
Expand All @@ -51,15 +55,21 @@ export default function getQueryFacts(

// Collect operations by their names.
const operations: OperationDefinitionNode[] = [];
documentAST.definitions.forEach(def => {
if (def.kind === 'OperationDefinition') {
operations.push(def);
}

visit(documentAST, {
OperationDefinition(node) {
operations.push(node);
},
});

return { variableToType, operations };
return { variableToType, operations, documentAST };
}

/**
* as a nod to folks who were clever enough to import this utility on their
*/
export const getQueryFacts = getOperationFacts;

/**
* Provided a schema and a document, produces a `variableToType` Object.
*/
Expand Down
5 changes: 5 additions & 0 deletions packages/graphiql/tsconfig.esm.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,10 @@
"**/*-test.js",
"**/*.stories.js",
"**/*.stories.ts"
],
"references": [
{
"path": "../graphql-language-service/tsconfig.esm.json"
}
]
}
5 changes: 5 additions & 0 deletions packages/graphiql/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,10 @@
"**/*.stories.js",
"**/*.stories.ts",
"**/*.stories.tsx"
],
"references": [
{
"path": "../graphql-language-service"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ import {

import {
CompletionItem,
DefinitionQueryResult,
Diagnostic,
Uri,
Position,
IPosition,
Outline,
OutlineTree,
GraphQLCache,
Expand All @@ -42,6 +41,7 @@ import {
getDefinitionQueryResultForFragmentSpread,
getDefinitionQueryResultForDefinitionNode,
getDefinitionQueryResultForNamedType,
DefinitionQueryResult,
} from './getDefinition';

import { getOutline } from './getOutline';
Expand Down Expand Up @@ -221,7 +221,7 @@ export class GraphQLLanguageService {

public async getAutocompleteSuggestions(
query: string,
position: Position,
position: IPosition,
filePath: Uri,
): Promise<Array<CompletionItem>> {
const projectConfig = this.getConfigForURI(filePath);
Expand All @@ -248,7 +248,7 @@ export class GraphQLLanguageService {

public async getHoverInformation(
query: string,
position: Position,
position: IPosition,
filePath: Uri,
): Promise<Hover['contents']> {
const projectConfig = this.getConfigForURI(filePath);
Expand All @@ -262,7 +262,7 @@ export class GraphQLLanguageService {

public async getDefinition(
query: string,
position: Position,
position: IPosition,
filePath: Uri,
): Promise<DefinitionQueryResult | null> {
const projectConfig = this.getConfigForURI(filePath);
Expand Down
Loading

0 comments on commit b9f3d7e

Please sign in to comment.