Skip to content

Commit

Permalink
feat: implied or external fragments, for #612 (#1750)
Browse files Browse the repository at this point in the history
allows external, implied fragments in autocompletion, validation, and operation execution. resolves #612 

- adds this capability to `monaco-graphql`, `codemirror-graphql`, and `graphiql`. it was already present in `graphql-language-service-interface`.
- provide an array of fragment definitions or a string containing fragment definitions
- only fragments you use are added in operation exec, thus in fetcher params as well
- still only shows valid autocomplete entries for fragment spread
- also exposes a documentAST of the current operation in `onEditQuery` and in the `fetcher`
  • Loading branch information
acao committed Jan 7, 2021
1 parent a804f3c commit cfed265
Show file tree
Hide file tree
Showing 47 changed files with 610 additions and 142 deletions.
79 changes: 79 additions & 0 deletions packages/codemirror-graphql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ CodeMirror helpers install themselves to the global CodeMirror when they
are imported.

```js
import type { ValidationContext, SDLValidationContext } from 'graphql';

import CodeMirror from 'codemirror';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/lint/lint';
Expand All @@ -30,6 +32,83 @@ CodeMirror.fromTextArea(myTextarea, {
mode: 'graphql',
lint: {
schema: myGraphQLSchema,
validationRules: [ExampleRule],
},
hintOptions: {
schema: myGraphQLSchema,
},
});
```

## External Fragments Example

If you want to have autcompletion for external fragment definitions, there's a new configuration setting available

```ts
import CodeMirror from 'codemirror';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/lint/lint';
import 'codemirror-graphql/hint';
import 'codemirror-graphql/lint';
import 'codemirror-graphql/mode';

const externalFragments = `
fragment MyFragment on Example {
id: ID!
name: String!
}
fragment AnotherFragment on Example {
id: ID!
title: String!
}
`;

CodeMirror.fromTextArea(myTextarea, {
mode: 'graphql',
lint: {
schema: myGraphQLSchema,
},
hintOptions: {
schema: myGraphQLSchema,
// here we use a string, but
// you can also provide an array of FragmentDefinitionNodes
externalFragments,
},
});
```

### Custom Validation Rules

If you want to show custom validation, you can do that too! It uses the `ValidationRule` interface.

```js
import type { ValidationRule } from 'graphql';

import CodeMirror from 'codemirror';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/lint/lint';
import 'codemirror-graphql/hint';
import 'codemirror-graphql/lint';
import 'codemirror-graphql/mode';

const ExampleRule: ValidationRule = context => {
// your custom rules here
const schema = context.getSchema();
const document = context.getDocument();
return {
NamedType(node) {
if (node.name.value !== node.name.value.toLowercase()) {
context.reportError('only lowercase type names allowed!');
}
},
};
};

CodeMirror.fromTextArea(myTextarea, {
mode: 'graphql',
lint: {
schema: myGraphQLSchema,
validationRules: [ExampleRule],
},
hintOptions: {
schema: myGraphQLSchema,
Expand Down
11 changes: 11 additions & 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: 'fragment Example on Test { id }',
},
});
}
Expand Down Expand Up @@ -766,6 +767,11 @@ describe('graphql-hint', () => {
type: TestType,
description: 'fragment Foo on Test',
},
{
text: 'Example',
type: TestType,
description: 'fragment Example on Test',
},
];
const expectedSuggestions = getExpectedSuggestions(list);
expect(suggestions.list).to.deep.equal(expectedSuggestions);
Expand All @@ -782,6 +788,11 @@ describe('graphql-hint', () => {
type: TestType,
description: 'fragment Foo on Test',
},
{
text: 'Example',
type: TestType,
description: 'fragment Example on Test',
},
];
const expectedSuggestions = getExpectedSuggestions(list);
expect(suggestions.list).to.deep.equal(expectedSuggestions);
Expand Down
5 changes: 4 additions & 1 deletion packages/codemirror-graphql/src/hint.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import CodeMirror from 'codemirror';
import { getAutocompleteSuggestions } from 'graphql-language-service-interface';
import { Position } from 'graphql-language-service-utils';

import { getFragmentDefinitions } from './utils/getFragmentDefinitions';
/**
* Registers a "hint" helper for CodeMirror.
*
Expand Down Expand Up @@ -52,6 +52,9 @@ CodeMirror.registerHelper('hint', 'graphql', (editor, options) => {
editor.getValue(),
position,
token,
Array.isArray(options.externalFragments)
? options.externalFragments
: getFragmentDefinitions(options.externalFragments),
);

const results = {
Expand Down
3 changes: 1 addition & 2 deletions packages/codemirror-graphql/src/lint.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ const TYPE = {
*/
CodeMirror.registerHelper('lint', 'graphql', (text, options) => {
const schema = options.schema;
const validationRules = options.validationRules;
const rawResults = getDiagnostics(text, schema, validationRules);
const rawResults = getDiagnostics(text, schema, options.validationRules);

const results = rawResults.map(error => ({
message: error.message,
Expand Down
12 changes: 12 additions & 0 deletions packages/codemirror-graphql/src/utils/getFragmentDefinitions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { visit, parse } from 'graphql';
import type { FragmentDefinitionNode } from 'graphql';

export function getFragmentDefinitions(graphqlString: string) {
const definitions: FragmentDefinitionNode[] = [];
visit(parse(graphqlString), {
FragmentDefinition(node) {
definitions.push(node);
},
});
return definitions;
}
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
3 changes: 2 additions & 1 deletion packages/graphiql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ For more details on props, see the [API Docs](https://graphiql-test.netlify.app/
| `query` | `string` (GraphQL) | initial displayed query, if `undefined` is provided, the stored query or `defaultQuery` will be used. You can also set this value at runtime to override the current operation editor state. |
| `validationRules` | `ValidationRule[]` | A array of validation rules that will be used for validating the GraphQL operations. If `undefined` is provided, the default rules (exported as `specifiedRules` from `graphql`) will be used. |
| `variables` | `string` (JSON) | initial displayed query variables, if `undefined` is provided, the stored variables will be used. |
| `headers` | `string` (JSON) | initial displayed request headers. if not defined, it will default to the stored headers if `shouldPersistHeaders` is enabled. |
| `headers` | `string` | initial displayed request headers. if not defined, it will default to the stored headers if `shouldPersistHeaders` is enabled. |
| `externalFragments` | `string | FragmentDefinitionNode[]` | provide fragments external to the operation for completion, validation, and for selective use when executing operations. |
| `operationName` | `string` | an optional name of which GraphQL operation should be executed. |
| `response` | `string` (JSON) | an optional JSON string to use as the initial displayed response. If not provided, no response will be initially shown. You might provide this if illustrating the result of the initial query. |
| `storage` | [`Storage`](https://graphiql-test.netlify.app/typedoc/interfaces/graphiql.storage.html) | **Default:** `window.localStorage`. an interface that matches `window.localStorage` signature that GraphiQL will use to persist state. |
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
Loading

0 comments on commit cfed265

Please sign in to comment.