diff --git a/src/execution/__tests__/directives-test.ts b/src/execution/__tests__/directives-test.ts index 92c8fb9c5f..78f5dd98fb 100644 --- a/src/execution/__tests__/directives-test.ts +++ b/src/execution/__tests__/directives-test.ts @@ -174,6 +174,7 @@ describe('Execute: handles directives', () => { data: { a: 'a', b: 'b' }, }); }); + it('unless false includes inline fragment', () => { const result = executeTestQuery(` query { @@ -188,6 +189,7 @@ describe('Execute: handles directives', () => { data: { a: 'a', b: 'b' }, }); }); + it('unless true includes inline fragment', () => { const result = executeTestQuery(` query { @@ -234,6 +236,7 @@ describe('Execute: handles directives', () => { data: { a: 'a', b: 'b' }, }); }); + it('unless false includes anonymous inline fragment', () => { const result = executeTestQuery(` query Q { @@ -248,6 +251,7 @@ describe('Execute: handles directives', () => { data: { a: 'a', b: 'b' }, }); }); + it('unless true includes anonymous inline fragment', () => { const result = executeTestQuery(` query { diff --git a/src/language/parser.ts b/src/language/parser.ts index 4c1725cce4..6f2fa925d4 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -165,6 +165,22 @@ export function parseType( return type; } +/** + * Parse the string containing a GraphQL directive (ex. `@foo(bar: "baz")`) into its AST. + * + * Throws GraphQLError if a syntax error is encountered. + */ +export function parseConstDirective( + source: string | Source, + options?: ParseOptions, +): ConstDirectiveNode { + const parser = new Parser(source, options); + parser.expectToken(TokenKind.SOF); + const directive = parser.parseDirective(true); + parser.expectToken(TokenKind.EOF); + return directive; +} + /** * This class is exported only to assist people in implementing their own parsers * without duplicating too much code and should be used only as last resort for cases diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index e6761926e5..e8726cfe92 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -6,27 +6,32 @@ import { dedent, dedentString } from '../../__testUtils__/dedent'; import { DirectiveLocation } from '../../language/directiveLocation'; import type { GraphQLFieldConfig } from '../../type/definition'; -import { GraphQLSchema } from '../../type/schema'; -import { GraphQLDirective } from '../../type/directives'; -import { GraphQLInt, GraphQLString, GraphQLBoolean } from '../../type/scalars'; import { + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLInterfaceType, GraphQLList, GraphQLNonNull, - GraphQLScalarType, GraphQLObjectType, - GraphQLInterfaceType, + GraphQLScalarType, GraphQLUnionType, - GraphQLEnumType, - GraphQLInputObjectType, } from '../../type/definition'; +import { GraphQLSchema, isSchema } from '../../type/schema'; +import { GraphQLDirective } from '../../type/directives'; +import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../../type/scalars'; import { buildSchema } from '../buildASTSchema'; -import { printSchema, printIntrospectionSchema } from '../printSchema'; +import type { PrintSchemaOptions } from '../printSchema'; +import { printIntrospectionSchema, printSchema } from '../printSchema'; +import { parseConstDirective } from '../../language/parser'; -function expectPrintedSchema(schema: GraphQLSchema) { - const schemaText = printSchema(schema); +function expectPrintedSchema( + schema: GraphQLSchema, + options?: PrintSchemaOptions, +) { + const schemaText = printSchema(schema, options); // keep printSchema and buildSchema in sync - expect(printSchema(buildSchema(schemaText))).to.equal(schemaText); + expect(printSchema(buildSchema(schemaText), options)).to.equal(schemaText); return expect(schemaText); } @@ -260,6 +265,16 @@ describe('Type System Printer', () => { `); }); + it('Omits schema of common names', () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ name: 'Query', fields: {} }), + }); + + expectPrintedSchema(schema).to.equal(dedent` + type Query + `); + }); + it('Prints schema with description', () => { const schema = new GraphQLSchema({ description: 'Schema description.', @@ -318,6 +333,68 @@ describe('Type System Printer', () => { `); }); + it('Prints schema with directives', () => { + const schema = buildSchema(` + directive @foo on SCHEMA | OBJECT + + type Query + `); + + expectPrintedSchema(schema, { + printDirectives: () => [parseConstDirective('@foo')], + }).to.equal(dedent` + schema @foo { + query: Query + } + + directive @foo on SCHEMA | OBJECT + + type Query @foo + `); + }); + + it('Includes directives conditionally', () => { + const schema = buildSchema(` + schema @foo { + query: Query + } + + directive @foo on SCHEMA + + directive @bar on SCHEMA + + type Query + `); + + expectPrintedSchema(schema, { + printDirectives: (definition) => { + if (isSchema(definition)) { + return [parseConstDirective('@bar')]; + } + + return []; + }, + }).to.equal(dedent` + schema @bar { + query: Query + } + + directive @foo on SCHEMA + + directive @bar on SCHEMA + + type Query + `); + + expectPrintedSchema(schema, { printDirectives: () => [] }).to.equal(dedent` + directive @foo on SCHEMA + + directive @bar on SCHEMA + + type Query + `); + }); + it('Print Interface', () => { const FooType = new GraphQLInterfaceType({ name: 'Foo', diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index 7f06542d4d..cfd61d22c5 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -8,44 +8,73 @@ import { isPrintableAsBlockString } from '../language/blockString'; import type { GraphQLSchema } from '../type/schema'; import type { GraphQLDirective } from '../type/directives'; +import { + DEFAULT_DEPRECATION_REASON, + isSpecifiedDirective, +} from '../type/directives'; import type { - GraphQLNamedType, GraphQLArgument, - GraphQLInputField, - GraphQLScalarType, GraphQLEnumType, - GraphQLObjectType, + GraphQLEnumValue, + GraphQLField, + GraphQLInputField, + GraphQLInputObjectType, GraphQLInterfaceType, + GraphQLNamedType, + GraphQLObjectType, + GraphQLScalarType, + GraphQLType, GraphQLUnionType, - GraphQLInputObjectType, } from '../type/definition'; -import { isIntrospectionType } from '../type/introspection'; -import { isSpecifiedScalarType } from '../type/scalars'; -import { - DEFAULT_DEPRECATION_REASON, - isSpecifiedDirective, -} from '../type/directives'; import { - isScalarType, - isObjectType, - isInterfaceType, - isUnionType, isEnumType, isInputObjectType, + isInterfaceType, + isObjectType, + isScalarType, + isUnionType, } from '../type/definition'; +import { isIntrospectionType } from '../type/introspection'; +import { isSpecifiedScalarType } from '../type/scalars'; + +import type { ConstDirectiveNode } from '../language/ast'; import { astFromValue } from './astFromValue'; -export function printSchema(schema: GraphQLSchema): string { +export interface PrintSchemaOptions { + printDirectives?: ( + definition: + | GraphQLSchema + | GraphQLType + | GraphQLField + | GraphQLEnumValue + | GraphQLInputField + | GraphQLArgument, + ) => ReadonlyArray; +} + +export function printSchema( + schema: GraphQLSchema, + options?: PrintSchemaOptions, +): string { return printFilteredSchema( schema, (n) => !isSpecifiedDirective(n), isDefinedType, + options, ); } -export function printIntrospectionSchema(schema: GraphQLSchema): string { - return printFilteredSchema(schema, isSpecifiedDirective, isIntrospectionType); +export function printIntrospectionSchema( + schema: GraphQLSchema, + options?: PrintSchemaOptions, +): string { + return printFilteredSchema( + schema, + isSpecifiedDirective, + isIntrospectionType, + options, + ); } function isDefinedType(type: GraphQLNamedType): boolean { @@ -56,21 +85,48 @@ function printFilteredSchema( schema: GraphQLSchema, directiveFilter: (type: GraphQLDirective) => boolean, typeFilter: (type: GraphQLNamedType) => boolean, + options?: PrintSchemaOptions, ): string { const directives = schema.getDirectives().filter(directiveFilter); const types = Object.values(schema.getTypeMap()).filter(typeFilter); return [ - printSchemaDefinition(schema), + printSchemaDefinition(schema, options), ...directives.map((directive) => printDirective(directive)), - ...types.map((type) => printType(type)), + ...types.map((type) => printType(type, options)), ] .filter(Boolean) .join('\n\n'); } -function printSchemaDefinition(schema: GraphQLSchema): Maybe { - if (schema.description == null && isSchemaOfCommonNames(schema)) { +function collectDirectives( + printDirectives: PrintSchemaOptions['printDirectives'] | undefined, + definition: + | GraphQLSchema + | GraphQLType + | GraphQLField + | GraphQLEnumValue + | GraphQLInputField + | GraphQLArgument, +) { + if (printDirectives == null) { + return []; + } + + return printDirectives(definition).map(print); +} + +function printSchemaDefinition( + schema: GraphQLSchema, + options?: PrintSchemaOptions, +): Maybe { + const directives = collectDirectives(options?.printDirectives, schema); + + if ( + schema.description == null && + directives.length === 0 && + isSchemaOfCommonNames(schema) + ) { return; } @@ -91,7 +147,12 @@ function printSchemaDefinition(schema: GraphQLSchema): Maybe { operationTypes.push(` subscription: ${subscriptionType.name}`); } - return printDescription(schema) + `schema {\n${operationTypes.join('\n')}\n}`; + return ( + printDescription(schema) + + ['schema', directives.join(' '), `{\n${operationTypes.join('\n')}\n}`] + .filter(Boolean) + .join(' ') + ); } /** @@ -128,12 +189,15 @@ function isSchemaOfCommonNames(schema: GraphQLSchema): boolean { return true; } -export function printType(type: GraphQLNamedType): string { +export function printType( + type: GraphQLNamedType, + options?: PrintSchemaOptions, +): string { if (isScalarType(type)) { return printScalar(type); } if (isObjectType(type)) { - return printObject(type); + return printObject(type, options); } if (isInterfaceType(type)) { return printInterface(type); @@ -167,10 +231,15 @@ function printImplementedInterfaces( : ''; } -function printObject(type: GraphQLObjectType): string { +function printObject( + type: GraphQLObjectType, + options?: PrintSchemaOptions, +): string { + const directives = collectDirectives(options?.printDirectives, type); return ( printDescription(type) + `type ${type.name}` + + (directives.length ? ' ' + directives.join(' ') : '') + printImplementedInterfaces(type) + printFields(type) );