From 4ee39b8cb17d185f66c97a86921f576991b96bb7 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Thu, 30 Jan 2020 19:49:49 +0800 Subject: [PATCH] introspection: Add support for repeatable directives Code is taken from #1541 --- src/type/__tests__/introspection-test.js | 25 +++++++++++++-- src/type/introspection.js | 4 +++ .../__tests__/buildClientSchema-test.js | 8 +++-- .../__tests__/getIntrospectionQuery-test.js | 32 +++++++++++++++++++ src/utilities/__tests__/schemaPrinter-test.js | 2 ++ src/utilities/buildClientSchema.js | 1 + src/utilities/getIntrospectionQuery.d.ts | 5 +++ src/utilities/getIntrospectionQuery.js | 20 ++++++++++-- src/utilities/introspectionFromSchema.js | 3 +- 9 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 src/utilities/__tests__/getIntrospectionQuery-test.js diff --git a/src/type/__tests__/introspection-test.js b/src/type/__tests__/introspection-test.js index dec5bff1c4..57bc151e17 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -27,7 +27,10 @@ describe('Introspection', () => { }, }), }); - const source = getIntrospectionQuery({ descriptions: false }); + const source = getIntrospectionQuery({ + descriptions: false, + directiveIsRepeatable: true, + }); const result = graphqlSync({ schema, source }); expect(result).to.deep.equal({ @@ -648,6 +651,21 @@ describe('Introspection', () => { isDeprecated: false, deprecationReason: null, }, + { + name: 'isRepeatable', + args: [], + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + }, + isDeprecated: false, + deprecationReason: null, + }, { name: 'locations', args: [], @@ -809,6 +827,7 @@ describe('Introspection', () => { directives: [ { name: 'include', + isRepeatable: false, locations: ['FIELD', 'FRAGMENT_SPREAD', 'INLINE_FRAGMENT'], args: [ { @@ -828,6 +847,7 @@ describe('Introspection', () => { }, { name: 'skip', + isRepeatable: false, locations: ['FIELD', 'FRAGMENT_SPREAD', 'INLINE_FRAGMENT'], args: [ { @@ -847,6 +867,7 @@ describe('Introspection', () => { }, { name: 'deprecated', + isRepeatable: false, locations: ['FIELD_DEFINITION', 'ENUM_VALUE'], args: [ { @@ -1394,7 +1415,7 @@ describe('Introspection', () => { }); const schema = new GraphQLSchema({ query: QueryRoot }); - const source = getIntrospectionQuery(); + const source = getIntrospectionQuery({ directiveIsRepeatable: true }); /* istanbul ignore next */ function fieldResolver(_1, _2, _3, info) { diff --git a/src/type/introspection.js b/src/type/introspection.js index cbfa2587ab..e4e93d530b 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -89,6 +89,10 @@ export const __Directive = new GraphQLObjectType({ type: GraphQLString, resolve: obj => obj.description, }, + isRepeatable: { + type: GraphQLNonNull(GraphQLBoolean), + resolve: obj => obj.isRepeatable, + }, locations: { type: GraphQLNonNull(GraphQLList(GraphQLNonNull(__DirectiveLocation))), resolve: obj => obj.locations, diff --git a/src/utilities/__tests__/buildClientSchema-test.js b/src/utilities/__tests__/buildClientSchema-test.js index 3c0559a7dd..e0d03922df 100644 --- a/src/utilities/__tests__/buildClientSchema-test.js +++ b/src/utilities/__tests__/buildClientSchema-test.js @@ -33,10 +33,12 @@ import { introspectionFromSchema } from '../introspectionFromSchema'; * returns that schema printed as SDL. */ function cycleIntrospection(sdlString: string): string { + const options = { directiveIsRepeatable: true }; + const serverSchema = buildSchema(sdlString); - const initialIntrospection = introspectionFromSchema(serverSchema); + const initialIntrospection = introspectionFromSchema(serverSchema, options); const clientSchema = buildClientSchema(initialIntrospection); - const secondIntrospection = introspectionFromSchema(clientSchema); + const secondIntrospection = introspectionFromSchema(clientSchema, options); /** * If the client then runs the introspection query against the client-side @@ -457,7 +459,7 @@ describe('Type System: build schema from introspection', () => { it('builds a schema with custom directives', () => { const sdl = dedent` """This is a custom directive""" - directive @customDirective on FIELD + directive @customDirective repeatable on FIELD type Query { string: String diff --git a/src/utilities/__tests__/getIntrospectionQuery-test.js b/src/utilities/__tests__/getIntrospectionQuery-test.js new file mode 100644 index 0000000000..ac382e5a18 --- /dev/null +++ b/src/utilities/__tests__/getIntrospectionQuery-test.js @@ -0,0 +1,32 @@ +// @flow strict + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { getIntrospectionQuery } from '../getIntrospectionQuery'; + +describe('getIntrospectionQuery', () => { + it('skip all "description" fields', () => { + expect(getIntrospectionQuery()).to.match(/\bdescription\b/); + + expect(getIntrospectionQuery({ descriptions: true })).to.match( + /\bdescription\b/, + ); + + expect(getIntrospectionQuery({ descriptions: false })).to.not.match( + /\bdescription\b/, + ); + }); + + it('include "isRepeatable" field', () => { + expect(getIntrospectionQuery()).to.not.match(/\bisRepeatable\b/); + + expect(getIntrospectionQuery({ directiveIsRepeatable: true })).to.match( + /\bisRepeatable\b/, + ); + + expect( + getIntrospectionQuery({ directiveIsRepeatable: false }), + ).to.not.match(/\bisRepeatable\b/); + }); +}); diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index 81c3feb2e7..c636a6439d 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -740,6 +740,7 @@ describe('Type System Printer', () => { type __Directive { name: String! description: String + isRepeatable: Boolean! locations: [__DirectiveLocation!]! args: [__InputValue!]! } @@ -927,6 +928,7 @@ describe('Type System Printer', () => { type __Directive { name: String! description: String + isRepeatable: Boolean! locations: [__DirectiveLocation!]! args: [__InputValue!]! } diff --git a/src/utilities/buildClientSchema.js b/src/utilities/buildClientSchema.js index bceb891f41..68b6f512d3 100644 --- a/src/utilities/buildClientSchema.js +++ b/src/utilities/buildClientSchema.js @@ -385,6 +385,7 @@ export function buildClientSchema( return new GraphQLDirective({ name: directiveIntrospection.name, description: directiveIntrospection.description, + isRepeatable: directiveIntrospection.isRepeatable, locations: directiveIntrospection.locations.slice(), args: buildInputValueDefMap(directiveIntrospection.args), }); diff --git a/src/utilities/getIntrospectionQuery.d.ts b/src/utilities/getIntrospectionQuery.d.ts index c18cd7a571..ece6b71db4 100644 --- a/src/utilities/getIntrospectionQuery.d.ts +++ b/src/utilities/getIntrospectionQuery.d.ts @@ -5,6 +5,10 @@ export interface IntrospectionOptions { // Whether to include descriptions in the introspection result. // Default: true descriptions: boolean; + + // Whether to include `isRepeatable` flag on directives. + // Default: false + directiveIsRepeatable?: boolean; } export function getIntrospectionQuery(options?: IntrospectionOptions): string; @@ -167,6 +171,7 @@ export interface IntrospectionEnumValue { export interface IntrospectionDirective { readonly name: string; readonly description?: Maybe; + readonly isRepeatable?: boolean; readonly locations: ReadonlyArray; readonly args: ReadonlyArray; } diff --git a/src/utilities/getIntrospectionQuery.js b/src/utilities/getIntrospectionQuery.js index e6d344086f..b5636c8421 100644 --- a/src/utilities/getIntrospectionQuery.js +++ b/src/utilities/getIntrospectionQuery.js @@ -5,11 +5,25 @@ import { type DirectiveLocationEnum } from '../language/directiveLocation'; export type IntrospectionOptions = {| // Whether to include descriptions in the introspection result. // Default: true - descriptions: boolean, + descriptions?: boolean, + + // Whether to include `isRepeatable` flag on directives. + // Default: false + directiveIsRepeatable?: boolean, |}; export function getIntrospectionQuery(options?: IntrospectionOptions): string { - const descriptions = options?.descriptions !== false ? 'description' : ''; + const optionsWithDefault = { + descriptions: true, + directiveIsRepeatable: false, + ...options, + }; + + const descriptions = optionsWithDefault.descriptions ? 'description' : ''; + const directiveIsRepeatable = optionsWithDefault.directiveIsRepeatable + ? 'isRepeatable' + : ''; + return ` query IntrospectionQuery { __schema { @@ -22,6 +36,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { directives { name ${descriptions} + ${directiveIsRepeatable} locations args { ...InputValue @@ -259,6 +274,7 @@ export type IntrospectionEnumValue = {| export type IntrospectionDirective = {| +name: string, +description?: ?string, + +isRepeatable?: boolean, +locations: $ReadOnlyArray, +args: $ReadOnlyArray, |}; diff --git a/src/utilities/introspectionFromSchema.js b/src/utilities/introspectionFromSchema.js index b3ff3d8e60..9668e8c5fe 100644 --- a/src/utilities/introspectionFromSchema.js +++ b/src/utilities/introspectionFromSchema.js @@ -26,7 +26,8 @@ export function introspectionFromSchema( schema: GraphQLSchema, options?: IntrospectionOptions, ): IntrospectionQuery { - const document = parse(getIntrospectionQuery(options)); + const optionsWithDefaults = { directiveIsRepeatable: true, ...options }; + const document = parse(getIntrospectionQuery(optionsWithDefaults)); const result = execute({ schema, document }); invariant(!isPromise(result) && !result.errors && result.data); return (result.data: any);