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

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
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
11 changes: 10 additions & 1 deletion src/language/ast.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Kind } from './kinds';
import { Kind } from './kinds';
import type { Source } from './source';
import type { TokenKind } from './tokenKind';

Expand Down Expand Up @@ -288,6 +288,15 @@ export function isNode(maybeNode: any): maybeNode is ASTNode {
return typeof maybeKind === 'string' && kindValues.has(maybeKind);
}

/**
* @internal
*/
export function isConstDirectiveNode(
maybeConstDirectiveNode: any,
): maybeConstDirectiveNode is ConstDirectiveNode {
return maybeConstDirectiveNode?.kind === Kind.DIRECTIVE;
}

/** Name */

export interface NameNode {
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
109 changes: 97 additions & 12 deletions src/utilities/__tests__/printSchema-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,36 @@ 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';

function expectPrintedSchema(schema: GraphQLSchema) {
const schemaText = printSchema(schema);
import type { PrintSchemaOptions } from '../printSchema';
import {
directivesFromAstNodes,
printIntrospectionSchema,
printSchema,
} from '../printSchema';
import { parseConstDirective } from '../../language/parser';

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 +269,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 +337,72 @@ describe('Type System Printer', () => {
`);
});

it('Prints schema with directives', () => {
const schema = buildSchema(`
schema @foo {
query: Query
}

directive @foo on SCHEMA

type Query
`);

expectPrintedSchema(schema, {
printDirectives: directivesFromAstNodes,
}).to.equal(dedent`
schema @foo {
query: Query
}

directive @foo on SCHEMA

type Query
`);
});

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
115 changes: 93 additions & 22 deletions src/utilities/printSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,47 +7,97 @@ import { print } from '../language/printer';
import { printBlockString } from '../language/blockString';

import type { GraphQLSchema } from '../type/schema';
import { isSchema } 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 { isConstDirectiveNode } 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,
);
}

/**
* Useful implementation of PrintSchemaOptions.printDirectives for users who
* define their schema through SDL and simply want to print all included directives.
*/
export const directivesFromAstNodes: PrintSchemaOptions['printDirectives'] = (
spawnia marked this conversation as resolved.
Show resolved Hide resolved
definition,
) => {
if (isSchema(definition)) {
return [
...(definition.astNode?.directives ?? []),
...(definition?.extensionASTNodes
?.map((node) => node.directives)
?.flat() ?? []),
].filter(isConstDirectiveNode);
}

return [];
};

function isDefinedType(type: GraphQLNamedType): boolean {
return !isSpecifiedScalarType(type) && !isIntrospectionType(type);
}
Expand All @@ -56,21 +106,37 @@ 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)),
]
.filter(Boolean)
.join('\n\n');
}

function printSchemaDefinition(schema: GraphQLSchema): Maybe<string> {
if (schema.description == null && isSchemaOfCommonNames(schema)) {
function printSchemaDefinition(
schema: GraphQLSchema,
options?: PrintSchemaOptions,
): Maybe<string> {
const directives: Array<string> = [];
const printDirectives = options?.printDirectives;
if (printDirectives) {
printDirectives(schema).forEach((directive) => {
directives.push(print(directive));
});
}

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

Expand All @@ -91,7 +157,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