Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option printDirective to printSchema #3362

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/execution/__tests__/directives-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ describe('Execute: handles directives', () => {
data: { a: 'a', b: 'b' },
});
});

it('unless false includes inline fragment', () => {
const result = executeTestQuery(`
query {
Expand All @@ -188,6 +189,7 @@ describe('Execute: handles directives', () => {
data: { a: 'a', b: 'b' },
});
});

it('unless true includes inline fragment', () => {
const result = executeTestQuery(`
query {
Expand Down Expand Up @@ -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 {
Expand All @@ -248,6 +251,7 @@ describe('Execute: handles directives', () => {
data: { a: 'a', b: 'b' },
});
});

it('unless true includes anonymous inline fragment', () => {
const result = executeTestQuery(`
query {
Expand Down
16 changes: 16 additions & 0 deletions src/language/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
99 changes: 88 additions & 11 deletions src/utilities/__tests__/printSchema-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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.',
Expand Down Expand Up @@ -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',
Expand Down
121 changes: 95 additions & 26 deletions src/utilities/printSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?: (
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps splitting this into separate monomorphic functions would make it easier for the end user and avoid them having to do type checks:

printDirectivesForSchema: (schema: GraphQLSchema): ReadonlyArray<ConstDirectiveNode>;
printDirectivesForType: (type: GraphQLType): ReadonlyArray<ConstDirectiveNode>;
...

definition:
| GraphQLSchema
| GraphQLType
| GraphQLField<unknown, unknown>
| GraphQLEnumValue
| GraphQLInputField
| GraphQLArgument,
) => ReadonlyArray<ConstDirectiveNode>;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there perhaps a better data structure to return? I don't want to go with string, as that puts the burden of proper formatting on the end users.

}

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 {
Expand All @@ -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<string> {
if (schema.description == null && isSchemaOfCommonNames(schema)) {
function collectDirectives(
printDirectives: PrintSchemaOptions['printDirectives'] | undefined,
definition:
| GraphQLSchema
| GraphQLType
| GraphQLField<unknown, unknown>
| GraphQLEnumValue
| GraphQLInputField
| GraphQLArgument,
) {
if (printDirectives == null) {
return [];
}

return printDirectives(definition).map(print);
}

function printSchemaDefinition(
schema: GraphQLSchema,
options?: PrintSchemaOptions,
): Maybe<string> {
const directives = collectDirectives(options?.printDirectives, schema);

if (
schema.description == null &&
directives.length === 0 &&
isSchemaOfCommonNames(schema)
) {
return;
}

Expand All @@ -91,7 +147,12 @@ function printSchemaDefinition(schema: GraphQLSchema): Maybe<string> {
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(' ')
);
}

/**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
);
Expand Down