Repository: kamilkisiela/graphql-inspector
Pull request #505 (2019-08-09 10:58:36)

Support Apollo directives
Related #228


Patch:
diff --git a/docs/essentials/validate.md b/docs/essentials/validate.md
index ce3fd280..ae182dfc 100644
--- a/docs/essentials/validate.md
+++ b/docs/essentials/validate.md
@@ -21,6 +21,7 @@ Run the following command:
 
 - `-d, --deprecated` - Fail on deprecated usage (default: _false_)
 - `--noStrictFragments` - Do not fail on duplicated fragment names (default: _false_)
+- `--apollo` - Support Apollo directives (@client and @connection) (default: _false_)
 - `--maxDepth <n>` - Fail when operation depth exceeds maximum depth (default: _false_)
 - `-r, --require 
` - require a module
 - `-t, --token 
` - an access token
diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts
index 4d5dcd69..e3c3a5be 100644
--- a/packages/cli/src/commands/validate.ts
+++ b/packages/cli/src/commands/validate.ts
@@ -18,6 +18,7 @@ export async function validate(
     require?: string[];
     deprecated: boolean;
     noStrictFragments: boolean;
+    apollo?: boolean;
     maxDepth?: number;
     renderer?: Renderer;
     headers?: Record<string, string>;
@@ -34,6 +35,7 @@ export async function validate(
     const invalidDocuments = validateDocuments(schema, documents, {
       strictFragments: !options.noStrictFragments,
       maxDepth: options.maxDepth || undefined,
+      apollo: options.apollo || false,
     });
 
     if (!invalidDocuments.length) {
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index 03f38193..050b292f 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -63,6 +63,7 @@ commander
   .option('--maxDepth <n>', 'Fail on deep operations', (val: string) =>
     parseInt(val, 10),
   )
+  .option('--apollo', 'Support Apollo directives', false)
   .description('Validate documents against a schema')
   .action((documents: string, schema: string, cmd: commander.Command) =>
     validate(documents, schema, normalizeOptions(cmd)),
diff --git a/packages/core/src/utils/apollo.ts b/packages/core/src/utils/apollo.ts
new file mode 100644
index 00000000..860b2a10
--- /dev/null
+++ b/packages/core/src/utils/apollo.ts
@@ -0,0 +1,22 @@
+import {DocumentNode, visit, parse, GraphQLSchema, extendSchema} from 'graphql';
+
+import {removeFieldIfDirectives} from './graphql';
+
+export function transformDocumentWithApollo(doc: DocumentNode): DocumentNode {
+  return visit(doc, {
+    Field(node) {
+      return removeFieldIfDirectives(node, ['client']);
+    },
+  });
+}
+
+export function transformSchemaWithApollo(
+  schema: GraphQLSchema,
+): GraphQLSchema {
+  return extendSchema(
+    schema,
+    parse(/* GraphQL */ `
+      directive @connection(key: String!, filter: [String]) on FIELD
+    `),
+  );
+}
diff --git a/packages/core/src/utils/graphql.ts b/packages/core/src/utils/graphql.ts
index 2d50ce7b..14c2705c 100644
--- a/packages/core/src/utils/graphql.ts
+++ b/packages/core/src/utils/graphql.ts
@@ -14,6 +14,7 @@ import {
   visit,
   visitWithTypeInfo,
   getNamedType,
+  FieldNode,
 } from 'graphql';
 
 export function safeChangeForField(
@@ -154,3 +155,18 @@ export function findDeprecatedUsages(
 
   return errors;
 }
+
+export function removeFieldIfDirectives(
+  node: FieldNode,
+  directiveNames: string[],
+): FieldNode | null {
+  if (node.directives) {
+    if (
+      node.directives.some(d => directiveNames.indexOf(d.name.value) !== -1)
+    ) {
+      return null;
+    }
+  }
+
+  return node;
+}
diff --git a/packages/core/src/validate/index.ts b/packages/core/src/validate/index.ts
index 9906ba5c..472093d9 100644
--- a/packages/core/src/validate/index.ts
+++ b/packages/core/src/validate/index.ts
@@ -13,6 +13,10 @@ import {DepGraph} from 'dependency-graph';
 import {readDocument} from '../ast/document';
 import {findDeprecatedUsages} from '../utils/graphql';
 import {validateQueryDepth} from './query-depth';
+import {
+  transformSchemaWithApollo,
+  transformDocumentWithApollo,
+} from '../utils/apollo';
 
 export interface InvalidDocument {
   source: Source;
@@ -23,6 +27,7 @@ export interface InvalidDocument {
 export interface ValidateOptions {
   strictFragments?: boolean;
   strictDeprecated?: boolean;
+  apollo?: boolean;
   maxDepth?: number;
 }
 
@@ -34,6 +39,7 @@ export function validate(
   const config: ValidateOptions = {
     strictDeprecated: true,
     strictFragments: true,
+    apollo: false,
     ...options,
   };
   const invalidDocuments: InvalidDocument[] = [];
@@ -89,12 +95,23 @@ export function validate(
         definitions: [...docWithOperations.definitions, ...extractedFragments],
       };
 
-      const errors = (validateDocument(schema, merged) as GraphQLError[]) || [];
+      const transformedSchema = config.apollo
+        ? transformSchemaWithApollo(schema)
+        : schema;
+      const transformedDoc = config.apollo
+        ? transformDocumentWithApollo(merged)
+        : merged;
+
+      const errors =
+        (validateDocument(
+          transformedSchema,
+          transformedDoc,
+        ) as GraphQLError[]) || [];
 
       if (config.maxDepth) {
         const depthError = validateQueryDepth({
           source: doc.source,
-          doc: merged,
+          doc: transformedDoc,
           maxDepth: config.maxDepth,
           fragmentGraph: graph,
         });
@@ -105,7 +122,7 @@ export function validate(
       }
 
       const deprecated = config.strictDeprecated
-        ? findDeprecatedUsages(schema, parse(doc.source.body))
+        ? findDeprecatedUsages(transformedSchema, parse(doc.source.body))
         : [];
       const duplicatedFragments = config.strictFragments
         ? findDuplicatedFragments(fragmentNames)


Test patch:
diff --git a/packages/core/__tests__/validate/apollo.ts b/packages/core/__tests__/validate/apollo.ts
new file mode 100644
index 00000000..709bf236
--- /dev/null
+++ b/packages/core/__tests__/validate/apollo.ts
@@ -0,0 +1,64 @@
+import {buildSchema, Source, print, parse} from 'graphql';
+
+import {validate} from '../../src/validate';
+
+describe('apollo', () => {
+  test('should remove a filed with @client', () => {
+    const schema = buildSchema(/* GraphQL */ `
+      type Post {
+        id: ID
+        title: String
+      }
+
+      type Query {
+        post: Post
+      }
+    `);
+
+    const doc = parse(/* GraphQL */ `
+      query getPost {
+        post {
+          id
+          title
+          extra @client
+        }
+      }
+    `);
+
+    const results = validate(schema, [new Source(print(doc))], {
+      apollo: true,
+    });
+
+    expect(results).toHaveLength(0);
+  });
+
+  test('should include @connection', () => {
+    const schema = buildSchema(/* GraphQL */ `
+      type Post {
+        id: ID
+        title: String
+        comments: [String]
+      }
+
+      type Query {
+        post(id: ID!): Post
+      }
+    `);
+
+    const doc = parse(/* GraphQL */ `
+      query getPost {
+        post(id: 1) {
+          id
+          title
+          comments @connection(key: "comments")
+        }
+      }
+    `);
+
+    const results = validate(schema, [new Source(print(doc))], {
+      apollo: true,
+    });
+
+    expect(results).toHaveLength(0);
+  });
+});