@ardatan/relay-compiler
Version:
A compiler tool for building GraphQL-driven applications.
155 lines (134 loc) • 4.21 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 {generateIDField} = require('../core/SchemaUtils');
const {hasUnaliasedSelection} = require('./TransformUtils');
import type CompilerContext from '../core/CompilerContext';
import type {InlineFragment, LinkedField, ScalarField} from '../core/IR';
import type {CompositeTypeID} from '../core/Schema';
const ID = 'id';
const NODE_TYPE = 'Node';
type State = {|
idFieldForType: CompositeTypeID => ScalarField,
idFragmentForType: CompositeTypeID => InlineFragment,
|};
/**
* A transform that adds an `id` field on any type that has an id field but
* where there is no unaliased `id` selection.
*/
function generateIDFieldTransform(context: CompilerContext): CompilerContext {
const schema = context.getSchema();
const typeToIDField = new Map();
function idFieldForType(type: CompositeTypeID): ScalarField {
let idField = typeToIDField.get(type);
if (idField == null) {
idField = generateIDField(schema, type);
typeToIDField.set(type, idField);
}
return idField;
}
const typeToIDFragment = new Map();
function idFragmentForType(type: CompositeTypeID): InlineFragment {
let fragment = typeToIDFragment.get(type);
if (fragment == null) {
fragment = {
kind: 'InlineFragment',
directives: [],
loc: {kind: 'Generated'},
metadata: null,
selections: [idFieldForType(type)],
typeCondition: type,
};
typeToIDFragment.set(type, fragment);
}
return fragment;
}
const state = {
idFieldForType,
idFragmentForType,
};
return IRTransformer.transform(
context,
{
LinkedField: visitLinkedField,
},
() => state,
);
}
function visitLinkedField(field: LinkedField, state: State): LinkedField {
// $FlowFixMe[incompatible-use]
const transformedNode = this.traverse(field, state);
// If the field already has an unaliased `id` field, do nothing
if (hasUnaliasedSelection(field, ID)) {
return transformedNode;
}
// $FlowFixMe[incompatible-use]
const context: CompilerContext = this.getContext();
const schema = context.getSchema();
const unmodifiedType = schema.assertCompositeType(
schema.getRawType(field.type),
);
// If the field type has an `id` subfield add an `id` selection
if (
schema.canHaveSelections(unmodifiedType) &&
schema.hasId(unmodifiedType)
) {
return {
...transformedNode,
selections: [
...transformedNode.selections,
state.idFieldForType(unmodifiedType),
],
};
}
// If the field type is abstract, then generate a `... on Node { id }`
// fragment if *any* concrete type implements Node. Then generate a
// `... on PossibleType { id }` for every concrete type that does *not*
// implement `Node`
const nodeType = schema.getTypeFromString(NODE_TYPE);
if (!nodeType) {
return transformedNode;
}
const nodeInterface = schema.assertInterfaceType(nodeType);
if (schema.isAbstractType(unmodifiedType)) {
const selections = [...transformedNode.selections];
if (schema.mayImplement(unmodifiedType, nodeInterface)) {
selections.push(state.idFragmentForType(nodeInterface));
}
Array.from(
schema
.getPossibleTypes(schema.assertAbstractType(unmodifiedType))
.values(),
)
.filter(
concreteType =>
!schema.implementsInterface(
schema.assertCompositeType(concreteType),
nodeInterface,
) && schema.hasId(concreteType),
)
.sort((a, b) =>
schema.getTypeString(a) < schema.getTypeString(b) ? -1 : 1,
)
.forEach(concreteType => {
selections.push(state.idFragmentForType(concreteType));
});
return {
...transformedNode,
selections,
};
}
return transformedNode;
}
module.exports = {
transform: generateIDFieldTransform,
};