From f24cc3ec15b1f03923e5ec86ab968f1cb9b3b3aa Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Fri, 31 Jan 2020 00:26:04 +0800 Subject: [PATCH] Add support for adding description to schema Implements https://github.com/graphql/graphql-spec/pull/466 --- src/__fixtures__/schema-kitchen-sink.graphql | 1 + src/language/__tests__/schema-parser-test.js | 19 +++++++++++++++ src/language/__tests__/schema-printer-test.js | 1 + src/language/ast.d.ts | 1 + src/language/ast.js | 1 + src/language/parser.js | 4 +++- src/language/printer.js | 3 ++- src/language/visitor.d.ts | 2 +- src/language/visitor.js | 2 +- src/type/__tests__/introspection-test.js | 16 +++++++++++++ src/type/__tests__/schema-test.js | 8 +++++++ src/type/introspection.js | 4 ++++ src/type/schema.d.ts | 3 +++ src/type/schema.js | 5 ++++ .../__tests__/buildASTSchema-test.js | 5 ++++ .../__tests__/buildClientSchema-test.js | 1 + .../__tests__/getIntrospectionQuery-test.js | 24 ++++++++++++++++++- src/utilities/__tests__/schemaPrinter-test.js | 21 ++++++++++++++++ src/utilities/buildClientSchema.js | 1 + src/utilities/extendSchema.js | 1 + src/utilities/getIntrospectionQuery.js | 12 +++++++++- src/utilities/introspectionFromSchema.js | 7 +++++- src/utilities/schemaPrinter.js | 6 +++-- 23 files changed, 139 insertions(+), 9 deletions(-) diff --git a/src/__fixtures__/schema-kitchen-sink.graphql b/src/__fixtures__/schema-kitchen-sink.graphql index 477e25d474..8ec1f2d8a6 100644 --- a/src/__fixtures__/schema-kitchen-sink.graphql +++ b/src/__fixtures__/schema-kitchen-sink.graphql @@ -1,3 +1,4 @@ +"""This is a description of the schema as a whole.""" schema { query: QueryType mutation: MutationType diff --git a/src/language/__tests__/schema-parser-test.js b/src/language/__tests__/schema-parser-test.js index 1cce0c75ff..a9577d12da 100644 --- a/src/language/__tests__/schema-parser-test.js +++ b/src/language/__tests__/schema-parser-test.js @@ -141,6 +141,25 @@ describe('Schema Parser', () => { ); }); + it('parses schema with description string', () => { + const doc = parse(dedent` + "Description" + schema { + query: Foo + } + `); + + expect(toJSONDeep(doc)).to.nested.deep.property( + 'definitions[0].description', + { + kind: 'StringValue', + value: 'Description', + block: false, + loc: { start: 0, end: 13 }, + }, + ); + }); + it('Description followed by something other than type system definition throws', () => { expectSyntaxError('"Description" 1').to.deep.equal({ message: 'Syntax Error: Unexpected Int "1".', diff --git a/src/language/__tests__/schema-printer-test.js b/src/language/__tests__/schema-printer-test.js index aa9dc74184..c1cbbf9ea5 100644 --- a/src/language/__tests__/schema-printer-test.js +++ b/src/language/__tests__/schema-printer-test.js @@ -38,6 +38,7 @@ describe('Printer: SDL document', () => { const printed = print(parse(kitchenSinkSDL)); expect(printed).to.equal(dedent` + """This is a description of the schema as a whole.""" schema { query: QueryType mutation: MutationType diff --git a/src/language/ast.d.ts b/src/language/ast.d.ts index d83bdfcafe..576db47b6c 100644 --- a/src/language/ast.d.ts +++ b/src/language/ast.d.ts @@ -405,6 +405,7 @@ export type TypeSystemDefinitionNode = export interface SchemaDefinitionNode { readonly kind: 'SchemaDefinition'; readonly loc?: Location; + readonly description?: StringValueNode; readonly directives?: ReadonlyArray; readonly operationTypes: ReadonlyArray; } diff --git a/src/language/ast.js b/src/language/ast.js index b38b5ffd76..9d5df64d86 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -441,6 +441,7 @@ export type TypeSystemDefinitionNode = export type SchemaDefinitionNode = {| +kind: 'SchemaDefinition', +loc?: Location, + +description?: StringValueNode, +directives?: $ReadOnlyArray, +operationTypes: $ReadOnlyArray, |}; diff --git a/src/language/parser.js b/src/language/parser.js index c76d6b7c40..faf8ff039d 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -772,10 +772,11 @@ class Parser { } /** - * SchemaDefinition : schema Directives[Const]? { OperationTypeDefinition+ } + * SchemaDefinition : Description? schema Directives[Const]? { OperationTypeDefinition+ } */ parseSchemaDefinition(): SchemaDefinitionNode { const start = this._lexer.token; + const description = this.parseDescription(); this.expectKeyword('schema'); const directives = this.parseDirectives(true); const operationTypes = this.many( @@ -785,6 +786,7 @@ class Parser { ); return { kind: Kind.SCHEMA_DEFINITION, + description, directives, operationTypes, loc: this.loc(start), diff --git a/src/language/printer.js b/src/language/printer.js index c66cbc0d96..71569529bd 100644 --- a/src/language/printer.js +++ b/src/language/printer.js @@ -106,8 +106,9 @@ const printDocASTReducer: any = { // Type System Definitions - SchemaDefinition: ({ directives, operationTypes }) => + SchemaDefinition: addDescription(({ directives, operationTypes }) => join(['schema', join(directives, ' '), block(operationTypes)], ' '), + ), OperationTypeDefinition: ({ operation, type }) => operation + ': ' + type, diff --git a/src/language/visitor.d.ts b/src/language/visitor.d.ts index ffe4a77807..d5b46182a6 100644 --- a/src/language/visitor.d.ts +++ b/src/language/visitor.d.ts @@ -101,7 +101,7 @@ export const QueryDocumentKeys: { ListType: ['type']; NonNullType: ['type']; - SchemaDefinition: ['directives', 'operationTypes']; + SchemaDefinition: ['description', 'directives', 'operationTypes']; OperationTypeDefinition: ['type']; ScalarTypeDefinition: ['description', 'name', 'directives']; diff --git a/src/language/visitor.js b/src/language/visitor.js index 68985d9c12..b5e2f85be7 100644 --- a/src/language/visitor.js +++ b/src/language/visitor.js @@ -92,7 +92,7 @@ export const QueryDocumentKeys: VisitorKeyMap = { ListType: ['type'], NonNullType: ['type'], - SchemaDefinition: ['directives', 'operationTypes'], + SchemaDefinition: ['description', 'directives', 'operationTypes'], OperationTypeDefinition: ['type'], ScalarTypeDefinition: ['description', 'name', 'directives'], diff --git a/src/type/__tests__/introspection-test.js b/src/type/__tests__/introspection-test.js index 57bc151e17..5c60e8a071 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -20,6 +20,7 @@ import { describe('Introspection', () => { it('executes an introspection query', () => { const schema = new GraphQLSchema({ + description: 'Sample schema', query: new GraphQLObjectType({ name: 'QueryRoot', fields: { @@ -85,6 +86,17 @@ describe('Introspection', () => { kind: 'OBJECT', name: '__Schema', fields: [ + { + name: 'description', + args: [], + type: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + isDeprecated: false, + deprecationReason: null, + }, { name: 'types', args: [], @@ -1304,6 +1316,10 @@ describe('Introspection', () => { description: 'A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.', fields: [ + { + name: 'description', + description: null, + }, { name: 'types', description: 'A list of all types supported by this server.', diff --git a/src/type/__tests__/schema-test.js b/src/type/__tests__/schema-test.js index 570ff2bd1e..b0ea3152d7 100644 --- a/src/type/__tests__/schema-test.js +++ b/src/type/__tests__/schema-test.js @@ -86,12 +86,20 @@ describe('Type System: Schema', () => { }); const schema = new GraphQLSchema({ + description: 'Sample schema', query: BlogQuery, mutation: BlogMutation, subscription: BlogSubscription, }); expect(printSchema(schema)).to.equal(dedent` + """Sample schema""" + schema { + query: Query + mutation: Mutation + subscription: Subscription + } + type Query { article(id: String): Article feed: [Article] diff --git a/src/type/introspection.js b/src/type/introspection.js index 2594b47d3b..043685475f 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -43,6 +43,10 @@ export const __Schema = new GraphQLObjectType({ 'A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.', fields: () => ({ + description: { + type: GraphQLString, + resolve: schema => schema.description, + }, types: { description: 'A list of all types supported by this server.', type: GraphQLNonNull(GraphQLList(GraphQLNonNull(__Type))), diff --git a/src/type/schema.d.ts b/src/type/schema.d.ts index c603fd4c41..be2cfb4e38 100644 --- a/src/type/schema.d.ts +++ b/src/type/schema.d.ts @@ -43,6 +43,7 @@ export function assertSchema(schema: any): GraphQLSchema; * */ export class GraphQLSchema { + description: Maybe; extensions: Maybe>>; astNode: Maybe; extensionASTNodes: Maybe>; @@ -104,6 +105,7 @@ export interface GraphQLSchemaValidationOptions { } export interface GraphQLSchemaConfig extends GraphQLSchemaValidationOptions { + description?: Maybe; query: Maybe; mutation?: Maybe; subscription?: Maybe; @@ -118,6 +120,7 @@ export interface GraphQLSchemaConfig extends GraphQLSchemaValidationOptions { * @internal */ export interface GraphQLSchemaNormalizedConfig extends GraphQLSchemaConfig { + description: Maybe; types: Array; directives: Array; extensions: Maybe>>; diff --git a/src/type/schema.js b/src/type/schema.js index 891f942bf7..330d7f716b 100644 --- a/src/type/schema.js +++ b/src/type/schema.js @@ -122,6 +122,7 @@ export function assertSchema(schema: mixed): GraphQLSchema { * */ export class GraphQLSchema { + description: ?string; extensions: ?ReadOnlyObjMap; astNode: ?SchemaDefinitionNode; extensionASTNodes: ?$ReadOnlyArray; @@ -157,6 +158,7 @@ export class GraphQLSchema { `${inspect(config.directives)}.`, ); + this.description = config.description; this.extensions = config.extensions && toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes; @@ -335,6 +337,7 @@ export class GraphQLSchema { toConfig(): GraphQLSchemaNormalizedConfig { return { + description: this.description, query: this.getQueryType(), mutation: this.getMutationType(), subscription: this.getSubscriptionType(), @@ -367,6 +370,7 @@ export type GraphQLSchemaValidationOptions = {| |}; export type GraphQLSchemaConfig = {| + description?: ?string, query?: ?GraphQLObjectType, mutation?: ?GraphQLObjectType, subscription?: ?GraphQLObjectType, @@ -383,6 +387,7 @@ export type GraphQLSchemaConfig = {| */ export type GraphQLSchemaNormalizedConfig = {| ...GraphQLSchemaConfig, + description: ?string, types: Array, directives: Array, extensions: ?ReadOnlyObjMap, diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index e4ea5a80b6..2d78eb0c33 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -158,6 +158,11 @@ describe('Schema Builder', () => { it('Supports descriptions', () => { const sdl = dedent` + """Do you agree that this is the most creative schema ever?""" + schema { + query: Query + } + """This is a directive""" directive @foo( """It has an argument""" diff --git a/src/utilities/__tests__/buildClientSchema-test.js b/src/utilities/__tests__/buildClientSchema-test.js index e0d03922df..a6831ba359 100644 --- a/src/utilities/__tests__/buildClientSchema-test.js +++ b/src/utilities/__tests__/buildClientSchema-test.js @@ -51,6 +51,7 @@ function cycleIntrospection(sdlString: string): string { describe('Type System: build schema from introspection', () => { it('builds a simple schema', () => { const sdl = dedent` + """Simple schema""" schema { query: Simple } diff --git a/src/utilities/__tests__/getIntrospectionQuery-test.js b/src/utilities/__tests__/getIntrospectionQuery-test.js index ac382e5a18..e25a906e19 100644 --- a/src/utilities/__tests__/getIntrospectionQuery-test.js +++ b/src/utilities/__tests__/getIntrospectionQuery-test.js @@ -18,7 +18,7 @@ describe('getIntrospectionQuery', () => { ); }); - it('include "isRepeatable" field', () => { + it('include "isRepeatable" field on directives', () => { expect(getIntrospectionQuery()).to.not.match(/\bisRepeatable\b/); expect(getIntrospectionQuery({ directiveIsRepeatable: true })).to.match( @@ -29,4 +29,26 @@ describe('getIntrospectionQuery', () => { getIntrospectionQuery({ directiveIsRepeatable: false }), ).to.not.match(/\bisRepeatable\b/); }); + + it('include "description" field on schema', () => { + expect(getIntrospectionQuery().match(/\bdescription\b/g)).to.have.lengthOf( + 5, + ); + + expect( + getIntrospectionQuery({ schemaDescription: false }).match( + /\bdescription\b/g, + ), + ).to.have.lengthOf(5); + + expect( + getIntrospectionQuery({ schemaDescription: true }).match( + /\bdescription\b/g, + ), + ).to.have.lengthOf(6); + + expect( + getIntrospectionQuery({ descriptions: false, schemaDescription: true }), + ).to.not.match(/\bdescription\b/); + }); }); diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index c636a6439d..ed7badbbca 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -256,6 +256,23 @@ describe('Type System Printer', () => { `); }); + it('Prints schema with description', () => { + const Schema = new GraphQLSchema({ + description: 'Schema description.', + query: new GraphQLObjectType({ name: 'Query', fields: {} }), + }); + + const output = printForTest(Schema); + expect(output).to.equal(dedent` + """Schema description.""" + schema { + query: Query + } + + type Query + `); + }); + it('Prints custom query root types', () => { const Schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'CustomType', fields: {} }), @@ -626,6 +643,8 @@ describe('Type System Printer', () => { A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. """ type __Schema { + description: String + """A list of all types supported by this server.""" types: [__Type!]! @@ -836,6 +855,8 @@ describe('Type System Printer', () => { # A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. type __Schema { + description: String + # A list of all types supported by this server. types: [__Type!]! diff --git a/src/utilities/buildClientSchema.js b/src/utilities/buildClientSchema.js index 68b6f512d3..788d602ad8 100644 --- a/src/utilities/buildClientSchema.js +++ b/src/utilities/buildClientSchema.js @@ -109,6 +109,7 @@ export function buildClientSchema( // Then produce and return a Schema with these types. return new GraphQLSchema({ + description: schemaIntrospection.description, query: queryType, mutation: mutationType, subscription: subscriptionType, diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index 69ac1f84ee..be93160dc2 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -219,6 +219,7 @@ export function extendSchemaImpl( // Then produce and return a Schema config with these types. return { + description: schemaDef?.description?.value, ...operationTypes, types: objectValues(typeMap), directives: [ diff --git a/src/utilities/getIntrospectionQuery.js b/src/utilities/getIntrospectionQuery.js index b5636c8421..8f79aace3b 100644 --- a/src/utilities/getIntrospectionQuery.js +++ b/src/utilities/getIntrospectionQuery.js @@ -7,15 +7,20 @@ export type IntrospectionOptions = {| // Default: true descriptions?: boolean, - // Whether to include `isRepeatable` flag on directives. + // Whether to include `isRepeatable` field on directives. // Default: false directiveIsRepeatable?: boolean, + + // Whether to include `description` field on schema. + // Default: false + schemaDescription?: boolean, |}; export function getIntrospectionQuery(options?: IntrospectionOptions): string { const optionsWithDefault = { descriptions: true, directiveIsRepeatable: false, + schemaDescription: false, ...options, }; @@ -23,10 +28,14 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { const directiveIsRepeatable = optionsWithDefault.directiveIsRepeatable ? 'isRepeatable' : ''; + const schemaDescription = optionsWithDefault.schemaDescription + ? descriptions + : ''; return ` query IntrospectionQuery { __schema { + ${schemaDescription} queryType { name } mutationType { name } subscriptionType { name } @@ -125,6 +134,7 @@ export type IntrospectionQuery = {| |}; export type IntrospectionSchema = {| + +description?: ?string, +queryType: IntrospectionNamedTypeRef, +mutationType: ?IntrospectionNamedTypeRef, +subscriptionType: ?IntrospectionNamedTypeRef, diff --git a/src/utilities/introspectionFromSchema.js b/src/utilities/introspectionFromSchema.js index 9668e8c5fe..7fe8dd60e1 100644 --- a/src/utilities/introspectionFromSchema.js +++ b/src/utilities/introspectionFromSchema.js @@ -26,7 +26,12 @@ export function introspectionFromSchema( schema: GraphQLSchema, options?: IntrospectionOptions, ): IntrospectionQuery { - const optionsWithDefaults = { directiveIsRepeatable: true, ...options }; + const optionsWithDefaults = { + directiveIsRepeatable: true, + schemaDescription: true, + ...options, + }; + const document = parse(getIntrospectionQuery(optionsWithDefaults)); const result = execute({ schema, document }); invariant(!isPromise(result) && !result.errors && result.data); diff --git a/src/utilities/schemaPrinter.js b/src/utilities/schemaPrinter.js index 6e26d4653d..674d5b26a5 100644 --- a/src/utilities/schemaPrinter.js +++ b/src/utilities/schemaPrinter.js @@ -99,7 +99,7 @@ function printFilteredSchema( } function printSchemaDefinition(schema: GraphQLSchema): ?string { - if (isSchemaOfCommonNames(schema)) { + if (schema.description == null && isSchemaOfCommonNames(schema)) { return; } @@ -120,7 +120,9 @@ function printSchemaDefinition(schema: GraphQLSchema): ?string { operationTypes.push(` subscription: ${subscriptionType.name}`); } - return `schema {\n${operationTypes.join('\n')}\n}`; + return ( + printDescription({}, schema) + `schema {\n${operationTypes.join('\n')}\n}` + ); } /**