/** * Forked from graphql-js schemaPrinter.js file @ v14.7.0 * This file has been modified to support printing federated * schema, including associated federation directives. */ import { GraphQLSchema, isSpecifiedDirective, isIntrospectionType, isSpecifiedScalarType, GraphQLNamedType, GraphQLDirective, isScalarType, isObjectType, isInterfaceType, isUnionType, isEnumType, isInputObjectType, GraphQLScalarType, GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, GraphQLEnumType, GraphQLInputObjectType, GraphQLArgument, GraphQLInputField, astFromValue, print, GraphQLField, GraphQLEnumValue, GraphQLString, DEFAULT_DEPRECATION_REASON, ASTNode, } from 'graphql'; import { Maybe } from '../composition'; import { isFederationType } from '../types'; import { isFederationDirective } from '../composition/utils'; import federationDirectives, { gatherDirectives } from '../directives'; type Options = { /** * Descriptions are defined as preceding string literals, however an older * experimental version of the SDL supported preceding comments as * descriptions. Set to true to enable this deprecated behavior. * This option is provided to ease adoption and will be removed in v16. * * Default: false */ commentDescriptions?: boolean; }; /** * Accepts options as a second argument: * * - commentDescriptions: * Provide true to use preceding comments as the description. * */ export function printSchema(schema: GraphQLSchema, options?: Options): string { return printFilteredSchema( schema, // Federation change: treat the directives defined by the federation spec // similarly to the directives defined by the GraphQL spec (ie, don't print // their definitions). (n) => !isSpecifiedDirective(n) && !isFederationDirective(n), isDefinedType, options, ); } export function printIntrospectionSchema( schema: GraphQLSchema, options?: Options, ): string { return printFilteredSchema( schema, isSpecifiedDirective, isIntrospectionType, options, ); } // Federation change: treat the types defined by the federation spec // similarly to the directives defined by the GraphQL spec (ie, don't print // their definitions). function isDefinedType(type: GraphQLNamedType): boolean { return ( !isSpecifiedScalarType(type) && !isIntrospectionType(type) && !isFederationType(type) ); } function printFilteredSchema( schema: GraphQLSchema, directiveFilter: (type: GraphQLDirective) => boolean, typeFilter: (type: GraphQLNamedType) => boolean, options?: Options, ): string { const directives = schema.getDirectives().filter(directiveFilter); const types = Object.values(schema.getTypeMap()) .sort((type1, type2) => type1.name.localeCompare(type2.name)) .filter(typeFilter); return ( [printSchemaDefinition(schema)] .concat( directives.map(directive => printDirective(directive, options)), types.map(type => printType(type, options)), ) .filter(Boolean) .join('\n\n') + '\n' ); } function printSchemaDefinition(schema: GraphQLSchema): string | undefined { if (isSchemaOfCommonNames(schema)) { return; } const operationTypes = []; const queryType = schema.getQueryType(); if (queryType) { operationTypes.push(` query: ${queryType.name}`); } const mutationType = schema.getMutationType(); if (mutationType) { operationTypes.push(` mutation: ${mutationType.name}`); } const subscriptionType = schema.getSubscriptionType(); if (subscriptionType) { operationTypes.push(` subscription: ${subscriptionType.name}`); } return `schema {\n${operationTypes.join('\n')}\n}`; } /** * GraphQL schema define root types for each type of operation. These types are * the same as any other type and can be named in any manner, however there is * a common naming convention: * * schema { * query: Query * mutation: Mutation * } * * When using this naming convention, the schema description can be omitted. */ function isSchemaOfCommonNames(schema: GraphQLSchema): boolean { const queryType = schema.getQueryType(); if (queryType && queryType.name !== 'Query') { return false; } const mutationType = schema.getMutationType(); if (mutationType && mutationType.name !== 'Mutation') { return false; } const subscriptionType = schema.getSubscriptionType(); if (subscriptionType && subscriptionType.name !== 'Subscription') { return false; } return true; } export function printType(type: GraphQLNamedType, options?: Options): string { if (isScalarType(type)) { return printScalar(type, options); } else if (isObjectType(type)) { return printObject(type, options); } else if (isInterfaceType(type)) { return printInterface(type, options); } else if (isUnionType(type)) { return printUnion(type, options); } else if (isEnumType(type)) { return printEnum(type, options); } else if (isInputObjectType(type)) { return printInputObject(type, options); } throw Error('Unexpected type: ' + (type as GraphQLNamedType).toString()); } function printScalar(type: GraphQLScalarType, options?: Options): string { return printDescription(options, type) + `scalar ${type.name}`; } function printObject(type: GraphQLObjectType, options?: Options): string { const interfaces = type.getInterfaces(); const implementedInterfaces = interfaces.length ? ' implements ' + interfaces.map(i => i.name).join(' & ') : ''; // Federation change: print `extend` keyword on type extensions. // // The implementation assumes that an owned type will have fields defined // since that is required for a valid schema. Types that are *only* // extensions will not have fields on the astNode since that ast doesn't // exist. // // XXX revist extension checking const isExtension = type.extensionASTNodes && type.astNode && !type.astNode.fields; return ( printDescription(options, type) + (isExtension ? 'extend ' : '') + `type ${type.name}${implementedInterfaces}` + // Federation addition for printing @key usages printFederationDirectives(type) + printFields(options, type) ); } function printInterface(type: GraphQLInterfaceType, options?: Options): string { // Federation change: print `extend` keyword on type extensions. // See printObject for assumptions made. // // XXX revist extension checking const isExtension = type.extensionASTNodes && type.astNode && !type.astNode.fields; return ( printDescription(options, type) + (isExtension ? 'extend ' : '') + `interface ${type.name}` + // Federation change: graphql@14 doesn't support interfaces implementing interfaces // printImplementedInterfaces(type) + printFederationDirectives(type) + printFields(options, type) ); } function printUnion(type: GraphQLUnionType, options?: Options): string { const types = type.getTypes(); const possibleTypes = types.length ? ' = ' + types.join(' | ') : ''; return printDescription(options, type) + 'union ' + type.name + possibleTypes; } function printEnum(type: GraphQLEnumType, options?: Options): string { const values = type .getValues() .map( (value, i) => printDescription(options, value, ' ', !i) + ' ' + value.name + printDeprecated(value), ); return ( printDescription(options, type) + `enum ${type.name}` + printBlock(values) ); } function printInputObject(type: GraphQLInputObjectType, options?: Options): string { const fields = Object.values(type.getFields()).map( (f, i) => printDescription(options, f, ' ', !i) + ' ' + printInputValue(f), ); return ( printDescription(options, type) + `input ${type.name}` + printBlock(fields) ); } function printFields( options: Options | undefined, type: GraphQLObjectType | GraphQLInterfaceType, ) { const fields = Object.values(type.getFields()).map( (f, i) => printDescription(options, f, ' ', !i) + ' ' + f.name + printArgs(options, f.args, ' ') + ': ' + String(f.type) + printDeprecated(f) + printFederationDirectives(f), ); return printBlock(fields); } // Federation change: *do* print the usages of federation directives. function printFederationDirectives( type: GraphQLNamedType | GraphQLField, ): string { if (!type.astNode) return ''; if (isInputObjectType(type)) return ''; const allDirectives = gatherDirectives(type) .filter((n) => federationDirectives.some((fedDir) => fedDir.name === n.name.value), ) .map(print); const dedupedDirectives = [...new Set(allDirectives)]; return dedupedDirectives.length > 0 ? ' ' + dedupedDirectives.join(' ') : ''; } export function printWithReducedWhitespace(ast: ASTNode): string { return print(ast) .replace(/\s+/g, ' ') .trim(); } function printBlock(items: string[]) { return items.length !== 0 ? ' {\n' + items.join('\n') + '\n}' : ''; } function printArgs( options: Options | undefined, args: GraphQLArgument[], indentation = '', ) { if (args.length === 0) { return ''; } // If every arg does not have a description, print them on one line. if (args.every(arg => !arg.description)) { return '(' + args.map(printInputValue).join(', ') + ')'; } return ( '(\n' + args .map( (arg, i) => printDescription(options, arg, ' ' + indentation, !i) + ' ' + indentation + printInputValue(arg), ) .join('\n') + '\n' + indentation + ')' ); } function printInputValue(arg: GraphQLInputField) { const defaultAST = astFromValue(arg.defaultValue, arg.type); let argDecl = arg.name + ': ' + String(arg.type); if (defaultAST) { argDecl += ` = ${print(defaultAST)}`; } return argDecl; } function printDirective(directive: GraphQLDirective, options?: Options) { return ( printDescription(options, directive) + 'directive @' + directive.name + printArgs(options, directive.args) + (directive.isRepeatable ? ' repeatable' : '') + ' on ' + directive.locations.join(' | ') ); } function printDeprecated( fieldOrEnumVal: GraphQLField | GraphQLEnumValue, ) { if (!fieldOrEnumVal.isDeprecated) { return ''; } const reason = fieldOrEnumVal.deprecationReason; const reasonAST = astFromValue(reason, GraphQLString); if (reasonAST && reason !== '' && reason !== DEFAULT_DEPRECATION_REASON) { return ' @deprecated(reason: ' + print(reasonAST) + ')'; } return ' @deprecated'; } function printDescription }>( options: Options | undefined, def: T, indentation = '', firstInBlock = true, ): string { const { description } = def; if (description == null) { return ''; } if (options?.commentDescriptions === true) { return printDescriptionWithComments(description, indentation, firstInBlock); } const preferMultipleLines = description.length > 70; const blockString = printBlockString(description, '', preferMultipleLines); const prefix = indentation && !firstInBlock ? '\n' + indentation : indentation; return prefix + blockString.replace(/\n/g, '\n' + indentation) + '\n'; } function printDescriptionWithComments( description: string, indentation: string, firstInBlock: boolean, ) { const prefix = indentation && !firstInBlock ? '\n' : ''; const comment = description .split('\n') .map(line => indentation + (line !== '' ? '# ' + line : '#')) .join('\n'); return prefix + comment + '\n'; } /** * Print a block string in the indented block form by adding a leading and * trailing blank line. However, if a block string starts with whitespace and is * a single-line, adding a leading blank line would strip that whitespace. * * @internal */ export function printBlockString( value: string, indentation: string = '', preferMultipleLines: boolean = false, ): string { const isSingleLine = value.indexOf('\n') === -1; const hasLeadingSpace = value[0] === ' ' || value[0] === '\t'; const hasTrailingQuote = value[value.length - 1] === '"'; const hasTrailingSlash = value[value.length - 1] === '\\'; const printAsMultipleLines = !isSingleLine || hasTrailingQuote || hasTrailingSlash || preferMultipleLines; let result = ''; // Format a multi-line block quote to account for leading space. if (printAsMultipleLines && !(isSingleLine && hasLeadingSpace)) { result += '\n' + indentation; } result += indentation ? value.replace(/\n/g, '\n' + indentation) : value; if (printAsMultipleLines) { result += '\n'; } return '"""' + result.replace(/"""/g, '\\"""') + '"""'; }