@ardatan/relay-compiler
Version:
A compiler tool for building GraphQL-driven applications.
373 lines (293 loc) • 13.7 kB
JavaScript
/**
* 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.
*
*
* @format
*/
// flowlint ambiguous-object-type:error
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _createForOfIteratorHelper2 = _interopRequireDefault(require("@babel/runtime/helpers/createForOfIteratorHelper"));
var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread2"));
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var IRTransformer = require('../core/IRTransformer');
var partitionArray = require('../util/partitionArray');
var _require = require('../core/CompilerError'),
createUserError = _require.createUserError,
createCompilerError = _require.createCompilerError;
var _require2 = require('relay-runtime'),
RelayFeatureFlags = _require2.RelayFeatureFlags;
var SCHEMA_EXTENSION = "\n enum RequiredFieldAction {\n NONE\n LOG\n THROW\n }\n directive @required(\n action: RequiredFieldAction!\n ) on FIELD\n";
/**
* This transform rewrites ScalarField and LinkedField nodes with a @required
* directive into fields with the directives stripped and sets the `required`
* and `path` metadata values.
*/
function requiredFieldTransform(context) {
var schema = context.getSchema();
return IRTransformer.transform(context, {
LinkedField: visitLinkedField,
ScalarField: vistitScalarField,
InlineFragment: visitInlineFragment,
Fragment: visitFragment,
Root: visitRoot
}, function (node) {
return {
schema: schema,
documentName: node.name,
path: [],
pathRequiredMap: new Map(),
currentNodeRequiredChildren: new Map(),
requiredChildrenMap: new Map(),
parentAbstractInlineFragment: null
};
});
}
function visitFragment(fragment, state) {
// $FlowFixMe[incompatible-use]
return addChildrenCanBubbleMetadata(this.traverse(fragment, state), state);
}
function visitRoot(root, state) {
// $FlowFixMe[incompatible-use]
return addChildrenCanBubbleMetadata(this.traverse(root, state), state);
}
function visitInlineFragment(fragment, state) {
var _state$parentAbstract;
// Ideally we could allow @required when the direct parent inline fragment was
// on a concrete type, but we would need to solve this bug in our Flow type
// generation first: T65695438
var parentAbstractInlineFragment = (_state$parentAbstract = state.parentAbstractInlineFragment) !== null && _state$parentAbstract !== void 0 ? _state$parentAbstract : getAbstractInlineFragment(fragment, state.schema); // $FlowFixMe[incompatible-use]
return this.traverse(fragment, (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, state), {}, {
parentAbstractInlineFragment: parentAbstractInlineFragment
}));
}
function getAbstractInlineFragment(fragment, schema) {
var typeCondition = fragment.typeCondition;
if (schema.isAbstractType(typeCondition)) {
return fragment;
}
return null;
} // Convert action to a number so that we can numerically compare their severity.
function getActionSeverity(action) {
switch (action) {
case 'NONE':
return 0;
case 'LOG':
return 1;
case 'THROW':
return 2;
default:
action;
throw createCompilerError("Unhandled action type ".concat(action));
}
}
function visitLinkedField(field, state) {
var path = [].concat((0, _toConsumableArray2["default"])(state.path), [field.alias]);
var newState = (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, state), {}, {
currentNodeRequiredChildren: new Map(),
path: path,
parentAbstractInlineFragment: null
}); // $FlowFixMe[incompatible-use]
var newField = this.traverse(field, newState);
var pathName = path.join('.');
assertCompatibleRequiredChildren(field, pathName, newState);
newField = applyDirectives(newField, pathName, state.documentName);
assertCompatibleNullability(newField, pathName, newState.pathRequiredMap);
var directiveMetadata = getRequiredDirectiveMetadata(newField);
if (directiveMetadata != null) {
assertParentIsNotInvalidInlineFragmet(state.schema, directiveMetadata, state.parentAbstractInlineFragment);
state.currentNodeRequiredChildren.set(field.alias, newField);
var severity = getActionSeverity(directiveMetadata.action); // Assert that all @required children have at least this severity.
newState.currentNodeRequiredChildren.forEach(function (childField) {
var childMetadata = getRequiredDirectiveMetadata(childField);
if (childMetadata == null) {
return;
}
if (getActionSeverity(childMetadata.action) < severity) {
throw createUserError("The @required field [1] may not have an `action` less severe than that of its @required parent [2]. [1] should probably be `action: ".concat(directiveMetadata.action, "`."), [childMetadata.actionLoc, directiveMetadata.actionLoc]);
}
});
}
state.requiredChildrenMap.set(pathName, newState.currentNodeRequiredChildren);
return addChildrenCanBubbleMetadata(newField, newState);
}
function vistitScalarField(field, state) {
var pathName = [].concat((0, _toConsumableArray2["default"])(state.path), [field.alias]).join('.');
var newField = applyDirectives(field, pathName, state.documentName);
var directiveMetadata = getRequiredDirectiveMetadata(newField);
if (directiveMetadata != null) {
assertParentIsNotInvalidInlineFragmet(state.schema, directiveMetadata, state.parentAbstractInlineFragment);
state.currentNodeRequiredChildren.set(field.alias, newField);
}
assertCompatibleNullability(newField, pathName, state.pathRequiredMap);
return newField;
}
function addChildrenCanBubbleMetadata(node, state) {
var _iterator = (0, _createForOfIteratorHelper2["default"])(state.currentNodeRequiredChildren.values()),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var child = _step.value;
var requiredMetadata = getRequiredDirectiveMetadata(child);
if (requiredMetadata != null && requiredMetadata.action !== 'THROW') {
var metadata = (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, node.metadata), {}, {
childrenCanBubbleNull: true
});
return (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, node), {}, {
metadata: metadata
});
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
return node;
}
function assertParentIsNotInvalidInlineFragmet(schema, directiveMetadata, parentAbstractInlineFragment) {
if (parentAbstractInlineFragment == null) {
return;
}
var typeCondition = parentAbstractInlineFragment.typeCondition;
if (schema.isUnion(typeCondition)) {
throw createUserError('The @required directive [1] may not be used anywhere within an inline fragment on a union type [2].', [directiveMetadata.directiveLoc, parentAbstractInlineFragment.loc]);
} else if (schema.isInterface(typeCondition)) {
throw createUserError('The @required directive [1] may not be used anywhere within an inline fragment on an interface type [2].', [directiveMetadata.directiveLoc, parentAbstractInlineFragment.loc]);
} else {
throw createCompilerError('Unexpected abstract inline fragment type.', [parentAbstractInlineFragment.loc]);
}
} // Check that this field's nullability matches all other instances.
function assertCompatibleNullability(field, pathName, pathRequiredMap) {
var existingField = pathRequiredMap.get(pathName);
if (existingField == null) {
pathRequiredMap.set(pathName, field);
return;
}
var requiredMetadata = getRequiredDirectiveMetadata(field);
var existingRequiredMetadata = getRequiredDirectiveMetadata(existingField);
if ((requiredMetadata === null || requiredMetadata === void 0 ? void 0 : requiredMetadata.action) === (existingRequiredMetadata === null || existingRequiredMetadata === void 0 ? void 0 : existingRequiredMetadata.action)) {
return;
}
if (requiredMetadata == null) {
throw createUserError("The field \"".concat(field.alias, "\" is @required in [1] but not in [2]."), [existingField.loc, field.loc]);
}
if (existingRequiredMetadata == null) {
throw createUserError("The field \"".concat(field.alias, "\" is @required in [1] but not in [2]."), [field.loc, existingField.loc]);
}
throw createUserError("The field \"".concat(field.alias, "\" has a different @required action in [1] than in [2]."), [requiredMetadata.actionLoc, existingRequiredMetadata.actionLoc]);
} // Metadata is untyped, so we use this utility function to do the type coersion.
function getRequiredDirectiveMetadata(field) {
var _field$metadata;
return (_field$metadata = field.metadata) === null || _field$metadata === void 0 ? void 0 : _field$metadata.required;
} // Check that this field has the same required children as all other instances.
function assertCompatibleRequiredChildren(field, fieldPath, _ref) {
var currentNodeRequiredChildren = _ref.currentNodeRequiredChildren,
pathRequiredMap = _ref.pathRequiredMap,
requiredChildrenMap = _ref.requiredChildrenMap;
var previouslyRequiredChildren = requiredChildrenMap.get(fieldPath);
if (previouslyRequiredChildren == null) {
return;
} // Check if this field has a required child field which was previously omitted.
var _iterator2 = (0, _createForOfIteratorHelper2["default"])(currentNodeRequiredChildren),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var _step2$value = _step2.value,
path = _step2$value[0],
childField = _step2$value[1];
if (!previouslyRequiredChildren.has(path)) {
var otherParent = pathRequiredMap.get(fieldPath);
if (otherParent == null) {
throw createCompilerError("Could not find other parent node at path \"".concat(fieldPath, "\"."), [childField.loc]);
}
throw createMissingRequiredFieldError(childField, otherParent);
}
} // Check if a previous reference to this field had a required child field which we are missing.
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
var _iterator3 = (0, _createForOfIteratorHelper2["default"])(previouslyRequiredChildren),
_step3;
try {
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
var _step3$value = _step3.value,
_path = _step3$value[0],
_childField = _step3$value[1];
if (!currentNodeRequiredChildren.has(_path)) {
throw createMissingRequiredFieldError(_childField, field);
}
}
} catch (err) {
_iterator3.e(err);
} finally {
_iterator3.f();
}
}
function createMissingRequiredFieldError(requiredChild, missingParent) {
var alias = requiredChild.alias;
return createUserError("The field \"".concat(alias, "\" is marked as @required in [1] but is missing in [2]."), [requiredChild.loc, missingParent.loc]);
} // TODO T74397896: Remove prefix gating once @required is rolled out more broadly.
function featureIsEnabled(documentName) {
var featureFlag = RelayFeatureFlags.ENABLE_REQUIRED_DIRECTIVES;
if (typeof featureFlag === 'boolean') {
return featureFlag;
} else if (featureFlag === 'LIMITED') {
return documentName.startsWith('RelayRequiredTest');
} else if (typeof featureFlag === 'string') {
return featureFlag.split('|').some(function (prefix) {
return documentName.startsWith(prefix);
});
}
return false;
} // Strip and validate @required directives, and convert them to metadata.
function applyDirectives(field, pathName, documentName) {
var _partitionArray = partitionArray(field.directives, function (directive) {
return directive.name === 'required';
}),
requiredDirectives = _partitionArray[0],
otherDirectives = _partitionArray[1];
if (requiredDirectives.length === 0) {
return field;
}
if (!featureIsEnabled(documentName)) {
throw new createUserError( // Purposefully don't include details in this error message, since we
// don't want folks adopting this feature until it's been tested more.
'The @required directive is experimental and not yet supported for use in product code', requiredDirectives.map(function (x) {
return x.loc;
}));
}
if (requiredDirectives.length > 1) {
throw new createUserError('Did not expect multiple @required directives.', requiredDirectives.map(function (x) {
return x.loc;
}));
}
var requiredDirective = requiredDirectives[0];
var arg = requiredDirective.args[0]; // I would expect this check to be handled by the schema validation, but...
if (arg == null) {
throw createUserError('The @required directive requires an `action` argument.', [requiredDirective.loc]);
}
if (arg.value.kind !== 'Literal') {
throw createUserError('Expected @required `action` argument to be a literal.', [arg.value.loc]);
}
return (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, field), {}, {
directives: otherDirectives,
metadata: (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, field.metadata), {}, {
required: {
action: arg.value.value,
actionLoc: arg.loc,
directiveLoc: requiredDirective.loc,
path: pathName
}
})
});
} // Transform @required directive to metadata
module.exports = {
SCHEMA_EXTENSION: SCHEMA_EXTENSION,
transform: requiredFieldTransform
};