Skip to content

Commit

Permalink
feat: validate external fragments, add docs, tests
Browse files Browse the repository at this point in the history
  • Loading branch information
acao committed Jan 6, 2021
1 parent b9f3d7e commit 19dac51
Show file tree
Hide file tree
Showing 12 changed files with 115 additions and 12 deletions.
37 changes: 37 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,7 @@ CodeMirror.fromTextArea(myTextarea, {
mode: 'graphql',
lint: {
schema: myGraphQLSchema,
validationRules: [ExampleRule],
},
hintOptions: {
schema: myGraphQLSchema,
Expand Down Expand Up @@ -79,5 +82,39 @@ CodeMirror.fromTextArea(myTextarea, {
});
```

### Custom Validation Rules

```js
import type { ValidationContext, SDLValidationContext } 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 = (context: ValidationContext | SDLValidationContext) => {
// your custom rules here
const schema = context.getSchema();
const document = context.getDocument();
// do stuff
if (containsSomethingWeDontWant(document, schema)) {
context.reportError('Nope not here');
}
};

CodeMirror.fromTextArea(myTextarea, {
mode: 'graphql',
lint: {
schema: myGraphQLSchema,
validationRules: [ExampleRule],
},
hintOptions: {
schema: myGraphQLSchema,
},
});
```

Build for the web with [webpack](http://webpack.github.io/) or
[browserify](http://browserify.org/).
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
2 changes: 1 addition & 1 deletion packages/graphiql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ For more details on props, see the [API Docs](https://graphiql-test.netlify.app/
| `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` | 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 use when executing operations. |
| `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
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
parse,
} from 'graphql';

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

describe('collectVariables', () => {
const TestType = new GraphQLObjectType({
Expand Down
5 changes: 4 additions & 1 deletion packages/graphiql/tsconfig.esm.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
],
"references": [
{
"path": "../graphql-language-service/tsconfig.esm.json"
"path": "../graphql-language-service"
},
{
"path": "../graphql-language-service-utils"
}
]
}
3 changes: 3 additions & 0 deletions packages/graphiql/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"references": [
{
"path": "../graphql-language-service"
},
{
"path": "../graphql-language-service-utils"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
import { CompletionItem } from 'graphql-language-service-types';

import fs from 'fs';
import { buildSchema, GraphQLSchema } from 'graphql';
import {
buildSchema,
FragmentDefinitionNode,
GraphQLSchema,
parse,
} from 'graphql';
import { Position } from 'graphql-language-service-utils';
import path from 'path';

Expand All @@ -31,8 +36,15 @@ describe('getAutocompleteSuggestions', () => {
function testSuggestions(
query: string,
point: Position,
externalFragments?: FragmentDefinitionNode[],
): Array<CompletionItem> {
return getAutocompleteSuggestions(schema, query, point)
return getAutocompleteSuggestions(
schema,
query,
point,
null,
externalFragments,
)
.filter(
field => !['__schema', '__type'].some(name => name === field.label),
)
Expand Down Expand Up @@ -314,6 +326,28 @@ query name {
).toEqual([{ label: 'Foo', detail: 'Human' }]);
});

it('provides correct fragment name suggestions for external fragments', () => {
const externalFragments = parse(`
fragment CharacterDetails on Human {
name
}
fragment CharacterDetails2 on Human {
name
}
`).definitions as FragmentDefinitionNode[];

const result = testSuggestions(
'query { human(id: "1") { ... }}',
new Position(0, 28),
externalFragments,
);

expect(result).toEqual([
{ label: 'CharacterDetails', detail: 'Human' },
{ label: 'CharacterDetails2', detail: 'Human' },
]);
});

it('provides correct directive suggestions', () => {
expect(testSuggestions('{ test @ }', new Position(0, 8))).toEqual([
{ label: 'include' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,15 @@ describe('getDiagnostics', () => {
expect(errors).toHaveLength(1);
expect(errors[0].message).toEqual('No query allowed.');
});

it('validates with external fragments', () => {
const errors = getDiagnostics(
`query hero { hero { ...HeroGuy } }`,
schema,
[],
false,
'fragment HeroGuy on Human { id }',
);
expect(errors).toHaveLength(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ function getSuggestionsForFragmentSpread(
const defState = getDefinitionState(token.state);
const fragments = getFragmentDefinitions(queryText);

if (fragmentDefs) {
if (fragmentDefs && fragmentDefs.length > 0) {
fragments.push(...fragmentDefs);
}

Expand Down
16 changes: 16 additions & 0 deletions packages/graphql-language-service-interface/src/getDiagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
import {
ASTNode,
DocumentNode,
FragmentDefinitionNode,
GraphQLError,
GraphQLSchema,
Location,
SourceLocation,
ValidationRule,
print,
} from 'graphql';

import { findDeprecatedUsages, parse } from 'graphql';
Expand Down Expand Up @@ -62,8 +64,22 @@ export function getDiagnostics(
schema: GraphQLSchema | null | undefined = null,
customRules?: Array<ValidationRule>,
isRelayCompatMode?: boolean,
externalFragments?: FragmentDefinitionNode[] | string,
): Array<Diagnostic> {
let ast = null;
if (externalFragments) {
if (typeof externalFragments === 'string') {
query += '\n\n' + externalFragments;
} else {
query +=
'\n\n' +
externalFragments.reduce((agg, node) => {
agg += print(node) + '\n\n';
return agg;
}, '');
}
}

try {
ast = parse(query);
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/
import { tmpdir } from 'os';
import { SymbolKind } from 'vscode-languageserver';
import { Position, IRange } from 'graphql-language-service-utils';
import { Position, Range } from 'graphql-language-service-utils';

import { MessageProcessor } from '../MessageProcessor';
import { parseDocument } from '../parseDocument';
Expand Down Expand Up @@ -83,8 +83,8 @@ describe('MessageProcessor', () => {
{
representativeName: 'item',
kind: 'Field',
startPosition: { line: 1, character: 2 },
endPosition: { line: 1, character: 4 },
startPosition: new Position(1, 2),
endPosition: new Position(1, 4),
children: [],
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { parse } from 'graphql';
import { IPosition } from '../Range';
import { Position } from '../Range';
import { getASTNodeAtPosition, pointToOffset } from '../getASTNodeAtPosition';

const doc = `
Expand Down

0 comments on commit 19dac51

Please sign in to comment.