@ardatan/relay-compiler
Version:
A compiler tool for building GraphQL-driven applications.
267 lines (247 loc) • 7.91 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 strict-local
* @format
*/
// flowlint ambiguous-object-type:error
;
const IRTransformer = require('../core/IRTransformer');
const getIdentifierForArgumentValue = require('../core/getIdentifierForArgumentValue');
const murmurHash = require('../util/murmurHash');
const {createUserError} = require('../core/CompilerError');
import type CompilerContext from '../core/CompilerContext';
import type {
Argument,
Defer,
Directive,
FragmentSpread,
InlineFragment,
LinkedField,
ScalarField,
Stream,
} from '../core/IR';
type State = {|
+documentName: string,
+recordLabel: (label: string, directive: Directive) => void,
|};
/**
* This transform finds usages of @defer and @stream, validates them, and
* converts the using node to specialized IR nodes (Defer/Stream).
*/
function deferStreamTransform(context: CompilerContext): CompilerContext {
return IRTransformer.transform(
context,
{
// TODO: type IRTransformer to allow changing result type
FragmentSpread: (visitFragmentSpread: $FlowFixMe),
// TODO: type IRTransformer to allow changing result type
InlineFragment: (visitInlineFragment: $FlowFixMe),
// TODO: type IRTransformer to allow changing result type
LinkedField: (visitLinkedField: $FlowFixMe),
ScalarField: visitScalarField,
},
sourceNode => {
const labels = new Map();
return {
documentName: sourceNode.name,
recordLabel: (label, directive) => {
const prevDirective = labels.get(label);
if (prevDirective) {
const labelArg = directive.args.find(({name}) => name === 'label');
const prevLabelArg = prevDirective.args.find(
({name}) => name === 'label',
);
const previousLocation = prevLabelArg?.loc ?? prevDirective.loc;
if (labelArg) {
throw createUserError(
`Invalid use of @${directive.name}, the provided label is ` +
"not unique. Specify a unique 'label' as a literal string.",
[labelArg?.loc, previousLocation],
);
} else {
throw createUserError(
`Invalid use of @${directive.name}, could not generate a ` +
"default label that is unique. Specify a unique 'label' " +
'as a literal string.',
[directive.loc, previousLocation],
);
}
}
labels.set(label, directive);
},
};
},
);
}
function visitLinkedField(
field: LinkedField,
state: State,
): LinkedField | Stream {
const context: CompilerContext = this.getContext();
const schema = context.getSchema();
let transformedField: LinkedField = this.traverse(field, state);
const streamDirective = transformedField.directives.find(
directive => directive.name === 'stream',
);
if (streamDirective == null) {
return transformedField;
}
const type = schema.getNullableType(field.type);
if (!schema.isList(type)) {
throw createUserError(
`Invalid use of @stream on non-plural field '${field.name}'`,
[streamDirective.loc],
);
}
transformedField = {
...transformedField,
directives: transformedField.directives.filter(
directive => directive.name !== 'stream',
),
};
const ifArg = streamDirective.args.find(arg => arg.name === 'if');
if (isLiteralFalse(ifArg)) {
return transformedField;
}
const initialCount = streamDirective.args.find(
arg => arg.name === 'initial_count',
);
if (initialCount == null) {
throw createUserError(
"Invalid use of @stream, the 'initial_count' argument is required.",
[streamDirective.loc],
);
}
const useCustomizedBatch = streamDirective.args.find(
arg => arg.name === 'use_customized_batch',
);
const label =
getLiteralStringArgument(streamDirective, 'label') ?? field.alias;
const transformedLabel = transformLabel(state.documentName, 'stream', label);
state.recordLabel(transformedLabel, streamDirective);
return {
if: ifArg?.value ?? null,
initialCount: initialCount.value,
useCustomizedBatch: useCustomizedBatch?.value ?? null,
kind: 'Stream',
label: transformedLabel,
loc: {kind: 'Derived', source: streamDirective.loc},
metadata: null,
selections: [transformedField],
};
}
function visitScalarField(field: ScalarField, state: State): ScalarField {
const streamDirective = field.directives.find(
directive => directive.name === 'stream',
);
if (streamDirective != null) {
throw createUserError(
`Invalid use of @stream on scalar field '${field.name}'`,
[streamDirective.loc],
);
}
// $FlowFixMe[incompatible-use]
return this.traverse(field, state);
}
function visitInlineFragment(
fragment: InlineFragment,
state: State,
): InlineFragment | Defer {
const deferDirective = fragment.directives.find(
directive => directive.name === 'defer',
);
if (deferDirective != null) {
throw createUserError(
'Invalid use of @defer on an inline fragment, @defer is only supported on fragment spreads.',
[fragment.loc],
);
}
return this.traverse(fragment, state);
}
function visitFragmentSpread(
spread: FragmentSpread,
state: State,
): FragmentSpread | Defer {
let transformedSpread: FragmentSpread = this.traverse(spread, state);
const deferDirective = transformedSpread.directives.find(
directive => directive.name === 'defer',
);
if (deferDirective == null) {
return transformedSpread;
}
transformedSpread = {
...transformedSpread,
directives: transformedSpread.directives.filter(
directive => directive.name !== 'defer',
),
};
const ifArg = deferDirective.args.find(arg => arg.name === 'if');
if (isLiteralFalse(ifArg)) {
return transformedSpread;
}
const label =
getLiteralStringArgument(deferDirective, 'label') ??
getFragmentSpreadName(spread);
const transformedLabel = transformLabel(state.documentName, 'defer', label);
state.recordLabel(transformedLabel, deferDirective);
return {
if: ifArg?.value ?? null,
kind: 'Defer',
label: transformedLabel,
loc: {kind: 'Derived', source: deferDirective.loc},
selections: [transformedSpread],
};
}
function getLiteralStringArgument(
directive: Directive,
argName: string,
): ?string {
const arg = directive.args.find(({name}) => name === argName);
if (arg == null) {
return null;
}
const value = arg.value.kind === 'Literal' ? arg.value.value : null;
if (value == null || typeof value !== 'string') {
throw createUserError(
`Expected the '${argName}' value to @${directive.name} to be a string literal if provided.`,
[arg.value.loc],
);
}
return value;
}
function transformLabel(
parentName: string,
directive: string,
label: string,
): string {
return `${parentName}$${directive}$${label}`;
}
function isLiteralFalse(arg: ?Argument): boolean {
return (
arg != null && arg.value.kind === 'Literal' && arg.value.value === false
);
}
function getFragmentSpreadName(fragmentSpread: FragmentSpread): string {
if (fragmentSpread.args.length === 0) {
return fragmentSpread.name;
}
const sortedArgs = [...fragmentSpread.args]
.sort((a, b) => {
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
})
.map(argument => {
return {
name: argument.name,
value: getIdentifierForArgumentValue(argument.value),
};
});
const hash = murmurHash(JSON.stringify(sortedArgs));
return `${fragmentSpread.name}_${hash}`;
}
module.exports = {
transform: deferStreamTransform,
};