@apollo/federation-internals
Version:
Apollo Federation internal utilities
432 lines (391 loc) • 18.5 kB
text/typescript
import {
ArgumentDefinition,
Directive,
DirectiveDefinition,
EnumType,
EnumValue,
ExtendableElement,
Extension,
FieldDefinition,
InputFieldDefinition,
InputObjectType,
InterfaceImplementation,
InterfaceType,
NamedType,
ObjectType,
RootType,
ScalarType,
Schema,
SchemaDefinition,
SchemaElement,
SchemaRootKind,
UnionMember,
UnionType
} from "./definitions";
import { assert } from "./utils";
import { valueToString } from "./values";
export type PrintOptions = {
indentString: string;
definitionsOrder: ('schema' | 'types' | 'directives')[],
rootTypesOrder: SchemaRootKind[],
typeCompareFn?: (t1: NamedType, t2: NamedType) => number;
implementedInterfaceCompareFn?: (t1: InterfaceImplementation<any>, t2: InterfaceImplementation<any>) => number;
fieldCompareFn?: (t1: FieldDefinition<any>, t2: FieldDefinition<any>) => number;
unionMemberCompareFn?: (t1: UnionMember, t2: UnionMember) => number;
enumValueCompareFn?: (t1: EnumValue, t2: EnumValue) => number;
inputObjectFieldCompareFn?: (t1: InputFieldDefinition, t2: InputFieldDefinition) => number;
directiveCompareFn?: (d1: DirectiveDefinition, d2: DirectiveDefinition) => number;
mergeTypesAndExtensions: boolean;
showAllBuiltIns: boolean;
noDescriptions: boolean;
directiveDefinitionFilter?: (d: DirectiveDefinition) => boolean,
typeFilter: (t: NamedType) => boolean,
fieldFilter: (f: FieldDefinition<any>) => boolean,
directiveApplicationFilter: (d: Directive) => boolean,
}
export const defaultPrintOptions: PrintOptions = {
indentString: " ",
definitionsOrder: ['schema', 'directives', 'types'],
rootTypesOrder: ['query', 'mutation', 'subscription'],
mergeTypesAndExtensions: false,
showAllBuiltIns: false,
noDescriptions: false,
typeFilter: () => true,
fieldFilter: () => true,
directiveApplicationFilter: () => true,
}
export function orderPrintedDefinitions(options: PrintOptions): PrintOptions {
return {
...options,
typeCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
implementedInterfaceCompareFn: (t1, t2) => t1.interface.name.localeCompare(t2.interface.name),
fieldCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
unionMemberCompareFn: (t1, t2) => t1.type.name.localeCompare(t2.type.name),
enumValueCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
inputObjectFieldCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
directiveCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
};
}
export function shallowOrderPrintedDefinitions(options: PrintOptions): PrintOptions {
return {
...options,
typeCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
directiveCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
};
}
function isDefinitionOrderValid(options: PrintOptions): boolean {
return options.definitionsOrder.length === 3
&& options.definitionsOrder.indexOf('schema') >= 0
&& options.definitionsOrder.indexOf('types') >= 0
&& options.definitionsOrder.indexOf('directives') >= 0;
}
function validateOptions(options: PrintOptions) {
if (!isDefinitionOrderValid(options)) {
throw new Error(`'definitionsOrder' should be a 3-element array containing 'schema', 'types' and 'directives' in the desired order (got: [${options.definitionsOrder.join(', ')}])`);
}
}
export function printSchema(schema: Schema, options: PrintOptions = defaultPrintOptions): string {
validateOptions(options);
let directives = options.showAllBuiltIns ? schema.allDirectives() : schema.directives();
if (options.directiveDefinitionFilter) {
directives = directives.filter(options.directiveDefinitionFilter);
}
if (options.directiveCompareFn) {
directives = directives.concat().sort(options.directiveCompareFn);
}
let types = options.showAllBuiltIns ? schema.allTypes() : schema.types();
if (options.typeFilter) {
types = types.filter(options.typeFilter);
}
if (options.typeCompareFn) {
types = types.concat().sort(options.typeCompareFn);
}
const definitions: string[][] = new Array(3);
definitions[options.definitionsOrder.indexOf('schema')] = printSchemaDefinitionAndExtensions(schema.schemaDefinition, options);
definitions[options.definitionsOrder.indexOf('directives')] = directives.map(directive => printDirectiveDefinition(directive, options));
definitions[options.definitionsOrder.indexOf('types')] = types.flatMap(type => printTypeDefinitionAndExtensions(type, options));
return definitions.flat().join('\n\n');
}
function definitionAndExtensions<T extends ExtendableElement>(element: {extensions(): readonly Extension<T>[]}, options: PrintOptions): (Extension<any> | null | undefined)[] {
return options.mergeTypesAndExtensions ? [undefined] : [null, ...element.extensions()];
}
function printSchemaDefinitionAndExtensions(schemaDefinition: SchemaDefinition, options: PrintOptions): string[] {
return printDefinitionAndExtensions(schemaDefinition, options, printSchemaDefinitionOrExtension);
}
function printDefinitionAndExtensions<T extends {extensions(): readonly Extension<any>[]}>(
t: T,
options: PrintOptions,
printer: (t: T, options: PrintOptions, extension?: Extension<any> | null) => string | undefined
): string[] {
return definitionAndExtensions(t, options)
.map(ext => printer(t, options, ext))
.filter(v => v !== undefined) as string[];
}
function printIsExtension(extension?: Extension<any> | null): string {
return extension ? 'extend ' : '';
}
function forExtension<T extends {ofExtension(): Extension<any> | undefined}>(ts: readonly T[], extension?: Extension<any> |null): readonly T[] {
if (extension === undefined) {
return ts;
}
return ts.filter(r => (r.ofExtension() ?? null) === extension);
}
function orderRoots(roots: readonly RootType[], options: PrintOptions): RootType[] {
return roots.concat().sort((r1, r2) => options.rootTypesOrder.indexOf(r1.rootKind) - options.rootTypesOrder.indexOf(r2.rootKind));
}
function appliedDirectives(
element: SchemaElement<any, any>,
options: PrintOptions,
extension?: Extension<any> | null,
): readonly Directive[] {
let directives = forExtension(element.appliedDirectives, extension);
if (options.directiveApplicationFilter) {
directives = directives.filter(options.directiveApplicationFilter);
}
return directives;
}
function printSchemaDefinitionOrExtension(
schemaDefinition: SchemaDefinition,
options: PrintOptions,
extension?: Extension<SchemaDefinition> | null
): string | undefined {
const roots = forExtension(schemaDefinition.roots(), extension);
const directives = appliedDirectives(schemaDefinition, options, extension);
if (!roots.length && !directives.length) {
return undefined;
}
if (!extension && !directives.length && isSchemaOfCommonNames(schemaDefinition)) {
return undefined;
}
const rootEntries = orderRoots(roots, options).map((rootType) => `${options.indentString}${rootType.rootKind}: ${rootType.type}`);
// Note that that the description is never written with the extension as `extend schema` doesn _not_ support descriptions
return printDescription(schemaDefinition, options, extension)
+ printIsExtension(extension)
+ 'schema'
+ printAppliedDirectives(directives, options, true, rootEntries.length !== 0)
+ (directives.length === 0 ? ' ' : '')
+ (rootEntries.length === 0 ? '' : '{\n' + rootEntries.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: SchemaDefinition): boolean {
return !schema.description && schema.roots().every(r => r.isDefaultRootName());
}
/**
* Convenience function that assumes `printTypeDefinitionAndExtensions` returns a single result and return that result.
* Throw an error if there `printTypeDefinitionAndExtensions` returns multiple results.
*/
export function printType(type: NamedType, options: PrintOptions = defaultPrintOptions): string {
const definitionAndExtensions = printTypeDefinitionAndExtensions(type, options);
assert(definitionAndExtensions.length == 1, `Type ${type} is built from more than 1 definition or extension`);
return definitionAndExtensions[0];
}
export function printTypeDefinitionAndExtensions(type: NamedType, options: PrintOptions = defaultPrintOptions): string[] {
switch (type.kind) {
case 'ScalarType': return printDefinitionAndExtensions(type, options, printScalarDefinitionOrExtension);
case 'ObjectType': return printDefinitionAndExtensions(type, options, (t, options, ext) => printFieldBasedTypeDefinitionOrExtension('type', t, options, ext));
case 'InterfaceType': return printDefinitionAndExtensions(type, options, (t, options, ext) => printFieldBasedTypeDefinitionOrExtension('interface', t, options, ext));
case 'UnionType': return printDefinitionAndExtensions(type, options, printUnionDefinitionOrExtension);
case 'EnumType': return printDefinitionAndExtensions(type, options, printEnumDefinitionOrExtension);
case 'InputObjectType': return printDefinitionAndExtensions(type, options, printInputDefinitionOrExtension);
}
}
export function printDirectiveDefinition(directive: DirectiveDefinition, options: PrintOptions = defaultPrintOptions): string {
const locations = directive.locations.join(' | ');
return `${printDescription(directive, options, null)}directive ${directive}${printArgs(directive.arguments(), options)}${directive.repeatable ? ' repeatable' : ''} on ${locations}`;
}
function printAppliedDirectives(
appliedDirectives: readonly Directive<any>[],
options: PrintOptions,
onNewLines: boolean = false,
endWithNewLine: boolean = onNewLines
): string {
if (appliedDirectives.length == 0) {
return "";
}
const joinStr = onNewLines ? '\n' + options.indentString : ' ';
const directives = appliedDirectives.map(d => d.toString()).join(joinStr);
return onNewLines ? '\n' + options.indentString + directives + (endWithNewLine ? '\n' : '') : ' ' + directives;
}
function printDescription(
element: SchemaElement<any, any>,
options: PrintOptions,
extension: Extension<any> | undefined | null,
indentation: string = '',
firstInBlock: boolean = true
): string {
// Note that extensions cannot have descriptions (it's not syntactically valid)
if (extension || element.description === undefined || options.noDescriptions) {
return '';
}
const preferMultipleLines = element.description.length > 70;
const blockString = printBlockString(element.description, '', preferMultipleLines);
const prefix =
indentation && !firstInBlock ? '\n' + indentation : indentation;
return prefix + blockString.replace(/\n/g, '\n' + indentation) + '\n';
}
function printScalarDefinitionOrExtension(type: ScalarType, options: PrintOptions, extension?: Extension<any> | null): string | undefined {
const directives = appliedDirectives(type, options, extension);
if (extension && !directives.length) {
return undefined;
}
return `${printDescription(type, options, extension)}${printIsExtension(extension)}scalar ${type.name}${printAppliedDirectives(directives, options, true, false)}`
}
function printImplementedInterfaces(implementations: readonly InterfaceImplementation<any>[]): string {
return implementations.length
? ' implements ' + implementations.map(i => i.interface.name).join(' & ')
: '';
}
function printFieldBasedTypeDefinitionOrExtension(kind: string, type: ObjectType | InterfaceType, options: PrintOptions, extension?: Extension<any> | null): string | undefined {
const directives = appliedDirectives(type, options, extension);
let interfaces = forExtension<InterfaceImplementation<any>>(type.interfaceImplementations(), extension);
let fields = forExtension<FieldDefinition<any>>(type.fields(), extension);
if (options.fieldFilter) {
fields = fields.filter(options.fieldFilter);
}
if (!directives.length && !interfaces.length && !fields.length && (extension || !type.preserveEmptyDefinition)) {
return undefined;
}
if (options.implementedInterfaceCompareFn) {
interfaces = interfaces.concat().sort(options.implementedInterfaceCompareFn);
}
if (options.fieldCompareFn) {
fields = fields.concat().sort(options.fieldCompareFn);
}
return printDescription(type, options, extension)
+ printIsExtension(extension)
+ kind + ' ' + type
+ printImplementedInterfaces(interfaces)
+ printAppliedDirectives(directives, options, true, fields.length > 0)
+ (directives.length === 0 && fields.length > 0 ? ' ' : '')
+ printFields(fields, options);
}
function printUnionDefinitionOrExtension(type: UnionType, options: PrintOptions, extension?: Extension<any> | null): string | undefined {
const directives = appliedDirectives(type, options, extension);
let members = forExtension(type.members(), extension);
if (!directives.length && !members.length && (extension || !type.preserveEmptyDefinition)) {
return undefined;
}
if (options.unionMemberCompareFn) {
members = members.concat().sort(options.unionMemberCompareFn);
}
const possibleTypes = members.length ? ' = ' + members.map(m => m.type).join(' | ') : '';
return printDescription(type, options, extension)
+ printIsExtension(extension)
+ 'union ' + type
+ printAppliedDirectives(directives, options, true, members.length > 0)
+ possibleTypes;
}
function printEnumDefinitionOrExtension(type: EnumType, options: PrintOptions, extension?: Extension<any> | null): string | undefined {
const directives = appliedDirectives(type, options, extension);
let values = forExtension(type.values, extension);
if (!directives.length && !values.length && (extension || !type.preserveEmptyDefinition)) {
return undefined;
}
if (options.enumValueCompareFn) {
values = values.concat().sort(options.enumValueCompareFn);
}
const vals = values.map((v, i) =>
printDescription(v, options, extension, options.indentString, !i)
+ options.indentString
+ v
+ printAppliedDirectives(v.appliedDirectives, options));
return printDescription(type, options, extension)
+ printIsExtension(extension)
+ 'enum ' + type
+ printAppliedDirectives(directives, options, true, vals.length > 0)
+ (directives.length === 0 && vals.length > 0 ? ' ' : '')
+ printBlock(vals);
}
function printInputDefinitionOrExtension(type: InputObjectType, options: PrintOptions, extension?: Extension<any> | null): string | undefined {
const directives = appliedDirectives(type, options, extension);
let fields = forExtension(type.fields(), extension);
if (!directives.length && !fields.length && (extension || !type.preserveEmptyDefinition)) {
return undefined;
}
if (options.inputObjectFieldCompareFn) {
fields = fields.concat().sort(options.inputObjectFieldCompareFn);
}
return printDescription(type, options, extension)
+ printIsExtension(extension)
+ 'input ' + type
+ printAppliedDirectives(directives, options, true, fields.length > 0)
+ (directives.length === 0 && fields.length > 0 ? ' ' : '')
+ printFields(fields, options);
}
function printFields(fields: readonly (FieldDefinition<any> | InputFieldDefinition)[], options: PrintOptions): string {
return printBlock(fields.map((f, i) =>
printDescription(f, options, undefined, options.indentString, !i)
+ options.indentString
+ printField(f, options)
+ printAppliedDirectives(appliedDirectives(f, options), options)));
}
function printField(field: FieldDefinition<any> | InputFieldDefinition, options: PrintOptions): string {
const args = field.kind == 'FieldDefinition' ? printArgs(field.arguments(), options, options.indentString) : '';
const defaultValue = field.kind === 'InputFieldDefinition' && field.defaultValue !== undefined
? ' = ' + valueToString(field.defaultValue, field.type)
: '';
return `${field.name}${args}: ${field.type}${defaultValue}`;
}
function printArgs(args: readonly ArgumentDefinition<any>[], options: PrintOptions, indentation = '') {
if (args.length === 0) {
return '';
}
// If every arg does not have a description, print them on one line.
// Note: this line means that, for args, we skip empty descriptions (because the empty string is falsy). This is inconsistent with
// `printDescription` where we print such description in other places. _However_, this is what graphQL-js does as well, and for now,
// we'd rather not have things diverge because of just that.
if (args.every(arg => !arg.description)) {
return '(' + args.map(arg => printArg(arg, options)).join(', ') + ')';
}
const formattedArgs = args
.map((arg, i) => printDescription(arg, options, null, ' ' + indentation, !i) + ' ' + indentation + printArg(arg, options))
.join('\n');
return `(\n${formattedArgs}\n${indentation})`;
}
function printArg(arg: ArgumentDefinition<any>, options: PrintOptions) {
return `${arg}${printAppliedDirectives(appliedDirectives(arg, options), options)}`;
}
function printBlock(items: string[]): string {
return items.length !== 0 ? '{\n' + items.join('\n') + '\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.
*/
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, '\\"""') + '"""';
}