@ardatan/relay-compiler
Version:
A compiler tool for building GraphQL-driven applications.
479 lines (457 loc) • 14.5 kB
Flow
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
// flowlint ambiguous-object-type:error
;
const invariant = require('invariant');
const {DEFAULT_HANDLE_KEY} = require('../util/DefaultHandleKey');
import type {CompilerContextDocument} from './CompilerContext';
import type {
Argument,
ArgumentDefinition,
ArgumentValue,
Directive,
Field,
LocalArgumentDefinition,
Node,
Selection,
} from './IR';
import type {Schema, TypeID} from './Schema';
const INDENT = ' ';
/**
* Converts an IR node into a GraphQL string. Custom Relay
* extensions (directives) are not supported; to print fragments with
* variables or fragment spreads with arguments, transform the node
* prior to printing.
*/
function print(schema: Schema, node: CompilerContextDocument): string {
switch (node.kind) {
case 'Fragment':
return (
`fragment ${node.name} on ${schema.getTypeString(node.type)}` +
printFragmentArgumentDefinitions(schema, node.argumentDefinitions) +
printDirectives(schema, node.directives) +
printSelections(schema, node, '', {}) +
'\n'
);
case 'Root':
return (
`${node.operation} ${node.name}` +
printArgumentDefinitions(schema, node.argumentDefinitions) +
printDirectives(schema, node.directives) +
printSelections(schema, node, '', {}) +
'\n'
);
case 'SplitOperation':
return (
`SplitOperation ${node.name} on ${schema.getTypeString(node.type)}` +
printSelections(schema, node, '', {}) +
'\n'
);
default:
(node: empty);
invariant(false, 'IRPrinter: Unsupported IR node `%s`.', node.kind);
}
}
function printSelections(
schema: Schema,
node: Node,
indent: string,
options?: {
parentDirectives?: string,
isClientExtension?: boolean,
...
},
): string {
const selections = node.selections;
if (selections == null) {
return '';
}
const printed = selections.map(selection =>
printSelection(schema, selection, indent, options),
);
return printed.length
? ` {\n${indent + INDENT}${printed.join(
'\n' + indent + INDENT,
)}\n${indent}${options?.isClientExtension === true ? '# ' : ''}}`
: '';
}
/**
* Prints a field without subselections.
*/
function printField(
schema: Schema,
field: Field,
options?: {
parentDirectives?: string,
isClientExtension?: boolean,
...
},
): string {
const parentDirectives = options?.parentDirectives ?? '';
const isClientExtension = options?.isClientExtension === true;
return (
(isClientExtension ? '# ' : '') +
(field.alias === field.name
? field.name
: field.alias + ': ' + field.name) +
printArguments(schema, field.args) +
parentDirectives +
printDirectives(schema, field.directives) +
printHandles(schema, field)
);
}
function printSelection(
schema: Schema,
selection: Selection,
indent: string,
options?: {
parentDirectives?: string,
isClientExtension?: boolean,
...
},
): string {
let str;
const parentDirectives = options?.parentDirectives ?? '';
const isClientExtension = options?.isClientExtension === true;
if (selection.kind === 'LinkedField') {
str = printField(schema, selection, {parentDirectives, isClientExtension});
str += printSelections(schema, selection, indent + INDENT, {
isClientExtension,
});
} else if (selection.kind === 'ModuleImport') {
str = selection.selections
.map(matchSelection =>
printSelection(schema, matchSelection, indent, {
parentDirectives,
isClientExtension,
}),
)
.join('\n' + indent + INDENT);
} else if (selection.kind === 'ScalarField') {
str = printField(schema, selection, {parentDirectives, isClientExtension});
} else if (selection.kind === 'InlineFragment') {
str = '';
if (isClientExtension) {
str += '# ';
}
str += '... on ' + schema.getTypeString(selection.typeCondition);
str += parentDirectives;
str += printDirectives(schema, selection.directives);
str += printSelections(schema, selection, indent + INDENT, {
isClientExtension,
});
} else if (selection.kind === 'FragmentSpread') {
str = '';
if (isClientExtension) {
str += '# ';
}
str += '...' + selection.name;
str += parentDirectives;
str += printFragmentArguments(schema, selection.args);
str += printDirectives(schema, selection.directives);
} else if (selection.kind === 'InlineDataFragmentSpread') {
str =
`# ${selection.name} @inline` +
`\n${indent}${INDENT}...` +
parentDirectives +
printSelections(schema, selection, indent + INDENT, {});
} else if (selection.kind === 'Condition') {
const value = printValue(schema, selection.condition, null);
// For Flow
invariant(
value != null,
'IRPrinter: Expected a variable for condition, got a literal `null`.',
);
let condStr = selection.passingValue ? ' @include' : ' @skip';
condStr += '(if: ' + value + ')';
condStr += parentDirectives;
// For multi-selection conditions, pushes the condition down to each
const subSelections = selection.selections.map(sel =>
printSelection(schema, sel, indent, {
parentDirectives: condStr,
isClientExtension,
}),
);
str = subSelections.join('\n' + indent + INDENT);
} else if (selection.kind === 'Stream') {
let streamStr = parentDirectives;
streamStr += ` @stream(label: "${selection.label}"`;
if (selection.if !== null) {
streamStr += `, if: ${printValue(schema, selection.if, null) ?? ''}`;
}
if (selection.initialCount !== null) {
streamStr += `, initial_count: ${printValue(
schema,
selection.initialCount,
null,
) ?? ''}`;
}
if (selection.useCustomizedBatch !== null) {
streamStr += `, use_customized_batch: ${printValue(
schema,
selection.useCustomizedBatch,
null,
) ?? 'false'}`;
}
streamStr += ')';
const subSelections = selection.selections.map(sel =>
printSelection(schema, sel, indent, {
parentDirectives: streamStr,
isClientExtension,
}),
);
str = subSelections.join('\n' + INDENT);
} else if (selection.kind === 'Defer') {
let deferStr = parentDirectives;
deferStr += ` @defer(label: "${selection.label}"`;
if (selection.if !== null) {
deferStr += `, if: ${printValue(schema, selection.if, null) ?? ''}`;
}
deferStr += ')';
if (
selection.selections.every(
subSelection =>
subSelection.kind === 'InlineFragment' ||
subSelection.kind === 'FragmentSpread',
)
) {
const subSelections = selection.selections.map(sel =>
printSelection(schema, sel, indent, {
parentDirectives: deferStr,
isClientExtension,
}),
);
str = subSelections.join('\n' + INDENT);
} else {
str = '...' + deferStr;
str += printSelections(schema, selection, indent + INDENT, {
isClientExtension,
});
}
} else if (selection.kind === 'ClientExtension') {
invariant(
isClientExtension === false,
'IRPrinter: Did not expect to encounter a ClientExtension node ' +
'as a descendant of another ClientExtension node.',
);
str =
'# Client-only selections:\n' +
indent +
INDENT +
selection.selections
.map(sel =>
printSelection(schema, sel, indent, {
parentDirectives,
isClientExtension: true,
}),
)
.join('\n' + indent + INDENT);
} else {
(selection: empty);
invariant(false, 'IRPrinter: Unknown selection kind `%s`.', selection.kind);
}
return str;
}
function printArgumentDefinitions(
schema: Schema,
argumentDefinitions: $ReadOnlyArray<LocalArgumentDefinition>,
): string {
const printed = argumentDefinitions.map(def => {
let str = `$${def.name}: ${schema.getTypeString(def.type)}`;
if (def.defaultValue != null) {
str += ' = ' + printLiteral(schema, def.defaultValue, def.type);
}
return str;
});
return printed.length ? `(\n${INDENT}${printed.join('\n' + INDENT)}\n)` : '';
}
function printFragmentArgumentDefinitions(
schema: Schema,
argumentDefinitions: $ReadOnlyArray<ArgumentDefinition>,
): string {
let printed;
argumentDefinitions.forEach(def => {
if (def.kind !== 'LocalArgumentDefinition') {
return;
}
printed = printed || [];
let str = `${def.name}: {type: "${schema.getTypeString(def.type)}"`;
if (def.defaultValue != null) {
str += `, defaultValue: ${printLiteral(
schema,
def.defaultValue,
def.type,
)}`;
}
str += '}';
// $FlowFixMe[incompatible-use]
printed.push(str);
});
return printed && printed.length
? ` @argumentDefinitions(\n${INDENT}${printed.join('\n' + INDENT)}\n)`
: '';
}
function printHandles(schema: Schema, field: Field): string {
if (!field.handles) {
return '';
}
const printed = field.handles.map(handle => {
// For backward compatibility.
const key =
handle.key === DEFAULT_HANDLE_KEY ? '' : `, key: "${handle.key}"`;
const filters =
handle.filters == null
? ''
: `, filters: ${JSON.stringify(Array.from(handle.filters).sort())}`;
const handleArgs =
handle.handleArgs == null
? ''
: `, handleArgs: ${printArguments(schema, handle.handleArgs)}`;
return `@__clientField(handle: "${handle.name}"${key}${filters}${handleArgs})`;
});
return printed.length ? ' ' + printed.join(' ') : '';
}
function printDirectives(
schema: Schema,
directives: $ReadOnlyArray<Directive>,
): string {
const printed = directives.map(directive => {
return '@' + directive.name + printArguments(schema, directive.args);
});
return printed.length ? ' ' + printed.join(' ') : '';
}
function printFragmentArguments(
schema: Schema,
args: $ReadOnlyArray<Argument>,
) {
const printedArgs = printArguments(schema, args);
if (!printedArgs.length) {
return '';
}
return ` @arguments${printedArgs}`;
}
function printArguments(
schema: Schema,
args: $ReadOnlyArray<Argument>,
): string {
const printed = [];
args.forEach(arg => {
const printedValue = printValue(schema, arg.value, arg.type);
if (printedValue != null) {
printed.push(arg.name + ': ' + printedValue);
}
});
return printed.length ? '(' + printed.join(', ') + ')' : '';
}
function printValue(
schema: Schema,
value: ArgumentValue,
type: ?TypeID,
): ?string {
if (type != null && schema.isNonNull(type)) {
type = schema.getNullableType(type);
}
if (value.kind === 'Variable') {
return '$' + value.variableName;
} else if (value.kind === 'ObjectValue') {
const inputType = type != null ? schema.asInputObjectType(type) : null;
const pairs = value.fields
.map(field => {
const fieldConfig =
inputType != null
? schema.hasField(inputType, field.name)
? schema.getFieldConfig(schema.expectField(inputType, field.name))
: null
: null;
const innerValue = printValue(schema, field.value, fieldConfig?.type);
return innerValue == null ? null : field.name + ': ' + innerValue;
})
.filter(Boolean);
return '{' + pairs.join(', ') + '}';
} else if (value.kind === 'ListValue') {
invariant(
type && schema.isList(type),
'GraphQLIRPrinter: Need a type in order to print arrays.',
);
const innerType = schema.getListItemType(type);
return `[${value.items
.map(i => printValue(schema, i, innerType))
.join(', ')}]`;
} else if (value.value != null) {
return printLiteral(schema, value.value, type);
} else {
return null;
}
}
function printLiteral(schema: Schema, value: mixed, type: ?TypeID): string {
if (value == null) {
return JSON.stringify(value) ?? 'null';
}
if (type != null && schema.isNonNull(type)) {
type = schema.getNullableType(type);
}
if (type && schema.isEnum(type)) {
let result = schema.serialize(schema.assertEnumType(type), value);
if (result == null && typeof value === 'string') {
// For backwards compatibility, print invalid input values as-is. This
// can occur with literals defined as an @argumentDefinitions
// defaultValue.
result = value;
}
invariant(
typeof result === 'string',
'IRPrinter: Expected value of type %s to be a valid enum value, got `%s`.',
schema.getTypeString(type),
JSON.stringify(value) ?? 'null',
);
return result;
} else if (type && (schema.isId(type) || schema.isInt(type))) {
return JSON.stringify(value) ?? '';
} else if (type && schema.isScalar(type)) {
const result = schema.serialize(schema.assertScalarType(type), value);
return JSON.stringify(result) ?? '';
} else if (Array.isArray(value)) {
invariant(
type && schema.isList(type),
'IRPrinter: Need a type in order to print arrays.',
);
const itemType = schema.getListItemType(type);
return (
'[' +
value.map(item => printLiteral(schema, item, itemType)).join(', ') +
']'
);
} else if (type && schema.isList(type) && value != null) {
// Not an array, but still a list. Treat as list-of-one as per spec 3.1.7:
// http://facebook.github.io/graphql/October2016/#sec-Lists
return printLiteral(schema, value, schema.getListItemType(type));
} else if (typeof value === 'object' && value != null) {
const fields = [];
invariant(
type && schema.isInputObject(type),
'IRPrinter: Need an InputObject type to print objects.',
);
const inputType = schema.assertInputObjectType(type);
for (const key in value) {
if (value.hasOwnProperty(key)) {
const fieldConfig = schema.getFieldConfig(
schema.expectField(inputType, key),
);
fields.push(
key + ': ' + printLiteral(schema, value[key], fieldConfig.type),
);
}
}
return '{' + fields.join(', ') + '}';
} else {
return JSON.stringify(value) ?? 'null';
}
}
module.exports = {print, printField, printArguments, printDirectives};