@ardatan/relay-compiler
Version:
A compiler tool for building GraphQL-driven applications.
438 lines (366 loc) • 18 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
'use strict';
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread2"));
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var IRTransformer = require('../core/IRTransformer');
var getLiteralArgumentValues = require('../core/getLiteralArgumentValues');
var getNormalizationOperationName = require('../core/getNormalizationOperationName');
var _require = require('../core/CompilerError'),
createCompilerError = _require.createCompilerError,
createUserError = _require.createUserError;
var _require2 = require('relay-runtime'),
getModuleComponentKey = _require2.getModuleComponentKey,
getModuleOperationKey = _require2.getModuleOperationKey;
var SUPPORTED_ARGUMENT_NAME = 'supported';
var JS_FIELD_TYPE = 'JSDependency';
var JS_FIELD_MODULE_ARG = 'module';
var JS_FIELD_ID_ARG = 'id';
var JS_FIELD_NAME = 'js';
var SCHEMA_EXTENSION = "\n directive @match(key: String) on FIELD\n\n directive @module(\n name: String!\n ) on FRAGMENT_SPREAD\n";
/**
* This transform rewrites LinkedField nodes with @match and rewrites them
* into `LinkedField` nodes with a `supported` argument.
*/
function matchTransform(context) {
return IRTransformer.transform(context, {
// TODO: type IRTransformer to allow changing result type
FragmentSpread: visitFragmentSpread,
LinkedField: visitLinkedField,
InlineFragment: visitInlineFragment,
ScalarField: visitScalarField
}, function (node) {
return {
documentName: node.name,
matchesForPath: new Map(),
moduleKey: null,
parentType: node.type,
path: []
};
});
}
function visitInlineFragment(node, state) {
// $FlowFixMe[incompatible-use]
return this.traverse(node, (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, state), {}, {
parentType: node.typeCondition
}));
}
function visitScalarField(field) {
// $FlowFixMe[incompatible-use]
var context = this.getContext();
var schema = context.getSchema();
if (field.name === JS_FIELD_NAME) {
var jsModuleType = schema.getTypeFromString(JS_FIELD_TYPE);
if (jsModuleType == null || !schema.isServerType(jsModuleType)) {
throw new createUserError("'".concat(JS_FIELD_NAME, "' should be defined on the server schema."), [field.loc]);
}
if (schema.isScalar(jsModuleType) && schema.areEqualTypes(schema.getRawType(field.type), jsModuleType)) {
throw new createUserError("Direct use of the '".concat(JS_FIELD_NAME, "' field is not allowed, use ") + '@match/@module instead.', [field.loc]);
}
}
return field;
}
function visitLinkedField(node, state) {
// $FlowFixMe[incompatible-use]
var context = this.getContext();
var schema = context.getSchema();
var matchDirective = node.directives.find(function (directive) {
return directive.name === 'match';
});
var moduleKey = null;
if (matchDirective != null) {
var _getLiteralArgumentVa = getLiteralArgumentValues(matchDirective.args);
moduleKey = _getLiteralArgumentVa.key;
if (moduleKey != null && (typeof moduleKey !== 'string' || !moduleKey.startsWith(state.documentName))) {
var _matchDirective$args$;
throw createUserError("Expected the 'key' argument of @match to be a literal string starting " + "with the document name, e.g. '".concat(state.documentName, "_<localName>'."), [((_matchDirective$args$ = matchDirective.args.find(function (arg) {
return arg.name === 'key';
})) !== null && _matchDirective$args$ !== void 0 ? _matchDirective$args$ : matchDirective).loc]);
}
}
state.path.push(node); // $FlowFixMe[incompatible-use]
var transformedNode = this.traverse(node, (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, state), {}, {
moduleKey: moduleKey,
parentType: node.type
}));
state.path.pop();
if (matchDirective == null) {
return transformedNode;
}
var parentType = state.parentType;
var rawType = schema.getRawType(parentType);
if (!(schema.isInterface(rawType) || schema.isObject(rawType))) {
throw createUserError("@match used on incompatible field '".concat(transformedNode.name, "'.") + '@match may only be used with fields whose parent type is an ' + "interface or object, got invalid type '".concat(schema.getTypeString(parentType), "'."), [node.loc]);
}
var currentField = schema.getFieldConfig(schema.expectField(schema.assertCompositeType(rawType), transformedNode.name));
var supportedArgumentDefinition = currentField.args.find(function (_ref) {
var name = _ref.name;
return name === SUPPORTED_ARGUMENT_NAME;
});
if (supportedArgumentDefinition == null) {
if (moduleKey == null) {
throw createUserError('@match on a field without the `supported` argument is a no-op, please remove the `@match`.', [node.loc]);
}
return transformedNode;
}
var supportedArgType = schema.getNullableType(supportedArgumentDefinition.type);
var supportedArgOfType = supportedArgType != null && schema.isList(supportedArgType) ? schema.getListItemType(supportedArgType) : null;
if (supportedArgType == null || supportedArgOfType == null || !schema.isString(schema.getNullableType(supportedArgOfType))) {
throw createUserError("@match used on incompatible field '".concat(transformedNode.name, "'. ") + '@match may only be used with fields that accept a ' + "'supported: [String!]!' argument.", [node.loc]);
}
var rawFieldType = schema.getRawType(transformedNode.type);
if (!schema.isAbstractType(rawFieldType)) {
throw createUserError("@match used on incompatible field '".concat(transformedNode.name, "'.") + '@match may only be used with fields that return a union or interface.', [node.loc]);
}
var seenTypes = new Map();
var selections = [];
transformedNode.selections.forEach(function (matchSelection) {
if (matchSelection.kind === 'ScalarField' && matchSelection.name === '__typename') {
selections.push(matchSelection);
return;
}
var moduleImport = matchSelection.kind === 'InlineFragment' ? matchSelection.selections[0] : null;
if (matchSelection.kind !== 'InlineFragment' || moduleImport == null || moduleImport.kind !== 'ModuleImport') {
throw createUserError('Invalid @match selection: all selections should be ' + 'fragment spreads with @module.', [matchSelection.loc]);
}
var matchedType = matchSelection.typeCondition;
seenTypes.set(matchedType, matchSelection);
selections.push(matchSelection);
});
if (seenTypes.size === 0) {
throw createUserError('Invalid @match selection: expected at least one @module selection. ' + "Remove @match or add a '...Fragment @module()' selection.", [matchDirective.loc]);
}
var supportedArg = transformedNode.args.find(function (arg) {
return arg.name === SUPPORTED_ARGUMENT_NAME;
});
if (supportedArg != null) {
throw createUserError("Invalid @match selection: the '".concat(SUPPORTED_ARGUMENT_NAME, "' argument ") + 'is automatically added and cannot be supplied explicitly.', [supportedArg.loc]);
}
return {
kind: 'LinkedField',
alias: transformedNode.alias,
args: [].concat((0, _toConsumableArray2["default"])(transformedNode.args), [{
kind: 'Argument',
name: SUPPORTED_ARGUMENT_NAME,
type: supportedArgumentDefinition.type,
value: {
kind: 'Literal',
loc: node.loc,
value: Array.from(seenTypes.keys()).map(function (type) {
return schema.getTypeString(type);
})
},
loc: node.loc
}]),
connection: false,
directives: [],
handles: null,
loc: node.loc,
metadata: null,
name: transformedNode.name,
type: transformedNode.type,
selections: selections
};
} // Transform @module
function visitFragmentSpread(spread, _ref2) {
var _moduleDirective$args2, _moduleDirective$args3, _moduleDirective$args4, _moduleDirective$args5, _moduleDirective$args6, _moduleDirective$args7;
var documentName = _ref2.documentName,
path = _ref2.path,
matchesForPath = _ref2.matchesForPath,
moduleKeyFromParent = _ref2.moduleKey;
var transformedNode = this.traverse(spread);
var moduleDirective = transformedNode.directives.find(function (directive) {
return directive.name === 'module';
});
if (moduleDirective == null) {
return transformedNode;
}
if (spread.args.length !== 0) {
var _spread$args$;
throw createUserError('@module does not support @arguments.', [(_spread$args$ = spread.args[0]) === null || _spread$args$ === void 0 ? void 0 : _spread$args$.loc].filter(Boolean));
}
var context = this.getContext();
var schema = context.getSchema();
var jsModuleType = schema.asScalarFieldType(schema.getTypeFromString(JS_FIELD_TYPE));
if (jsModuleType == null || !schema.isServerType(jsModuleType)) {
throw new createUserError("'".concat(JS_FIELD_NAME, "' should be defined on the server schema."), [spread.loc]);
}
if (!schema.isScalar(jsModuleType)) {
throw createUserError('Using @module requires the schema to define a scalar ' + "'".concat(JS_FIELD_TYPE, "' type."));
}
var fragment = context.getFragment(spread.name, spread.loc);
if (!schema.isObject(fragment.type)) {
throw createUserError("@module used on invalid fragment spread '...".concat(spread.name, "'. @module ") + 'may only be used with fragments on a concrete (object) type, ' + "but the fragment has abstract type '".concat(schema.getTypeString(fragment.type), "'."), [spread.loc, fragment.loc]);
}
var field = schema.getFieldByName(fragment.type, JS_FIELD_NAME);
if (!field) {
throw createUserError("@module used on invalid fragment spread '...".concat(spread.name, "'. @module ") + "requires the fragment type '".concat(schema.getTypeString(fragment.type), "' to have a ") + "'".concat(JS_FIELD_NAME, "(").concat(JS_FIELD_MODULE_ARG, ": String! ") + "[".concat(JS_FIELD_ID_ARG, ": String]): ").concat(JS_FIELD_TYPE, "' field (your ") + "schema may choose to omit the 'id' argument but if present it " + "must accept a 'String').", [moduleDirective.loc]);
}
var jsField = schema.getFieldConfig(field);
var jsFieldModuleArg = jsField ? jsField.args.find(function (arg) {
return arg.name === JS_FIELD_MODULE_ARG;
}) : null;
var jsFieldIdArg = jsField ? jsField.args.find(function (arg) {
return arg.name === JS_FIELD_ID_ARG;
}) : null;
if (jsFieldModuleArg == null || !schema.isString(schema.getNullableType(jsFieldModuleArg.type)) || jsFieldIdArg != null && !schema.isString(jsFieldIdArg.type) || jsField.type !== jsModuleType) {
throw createUserError("@module used on invalid fragment spread '...".concat(spread.name, "'. @module ") + "requires the fragment type '".concat(schema.getTypeString(fragment.type), "' to have a ") + "'".concat(JS_FIELD_NAME, "(").concat(JS_FIELD_MODULE_ARG, ": String! ") + "[".concat(JS_FIELD_ID_ARG, ": String]): ").concat(JS_FIELD_TYPE, "' field (your ") + "schema may choose to omit the 'id' argument but if present it " + "must accept a 'String').", [moduleDirective.loc]);
}
if (spread.directives.length !== 1) {
throw createUserError("@module used on invalid fragment spread '...".concat(spread.name, "'. @module ") + 'may not have additional directives.', [spread.loc]);
}
var _getLiteralArgumentVa2 = getLiteralArgumentValues(moduleDirective.args),
moduleName = _getLiteralArgumentVa2.name;
if (typeof moduleName !== 'string') {
var _moduleDirective$args;
throw createUserError("Expected the 'name' argument of @module to be a literal string", [((_moduleDirective$args = moduleDirective.args.find(function (arg) {
return arg.name === 'name';
})) !== null && _moduleDirective$args !== void 0 ? _moduleDirective$args : spread).loc]);
}
var parentField = path[path.length - 1];
var moduleKey = moduleKeyFromParent !== null && moduleKeyFromParent !== void 0 ? moduleKeyFromParent : documentName;
var aliasPath = path.map(function (x) {
return x.alias;
}).join('.');
var moduleId = aliasPath === '' ? documentName : "".concat(documentName, ".").concat(aliasPath);
var typeName = schema.getTypeString(fragment.type);
var matches = matchesForPath.get(aliasPath);
if (matches == null) {
var _parentField$loc;
if (matchesForPath.size !== 0) {
var existingMatchWithKey = Array.from(matchesForPath.values()).find(function (entry) {
return entry.key === moduleKey;
});
if (existingMatchWithKey != null) {
if (parentField == null) {
throw createCompilerError('Cannot have @module selections at multiple paths unless the selections are within fields.', [spread.loc]);
}
throw createUserError('Invalid @module selection: documents with multiple fields ' + "containing 3D selections must specify a unique 'key' value " + "for each field: use '".concat(parentField.alias, " @match(key: \"").concat(documentName, "_<localName>\")'."), [parentField.loc]);
}
}
matches = {
key: moduleKey,
location: (_parentField$loc = parentField === null || parentField === void 0 ? void 0 : parentField.loc) !== null && _parentField$loc !== void 0 ? _parentField$loc : spread.loc,
types: new Map()
};
matchesForPath.set(aliasPath, matches);
}
if (moduleKey !== matches.key) {
var _parentField$loc2;
// The user can't override the key locally (per @module),
// so this is just an internal sanity check
throw createCompilerError('Invalid @module selection: expected all selections at path ' + "'".concat(aliasPath, " to have the same 'key', got '").concat(moduleKey, "' and '").concat(matches.key, "'."), [(_parentField$loc2 = parentField === null || parentField === void 0 ? void 0 : parentField.loc) !== null && _parentField$loc2 !== void 0 ? _parentField$loc2 : spread.loc]);
}
var previousMatchForType = matches.types.get(typeName);
if (previousMatchForType != null && (previousMatchForType.fragment !== spread.name || previousMatchForType.module !== moduleName)) {
throw createUserError('Invalid @module selection: concrete type ' + "'".concat(typeName, "' was matched multiple times at path ") + "'".concat(aliasPath, "' but with a different fragment or module name."), [spread.loc, previousMatchForType.location]);
}
matches.types.set(typeName, {
location: spread.loc,
fragment: spread.name,
module: moduleName
});
var normalizationName = getNormalizationOperationName(spread.name) + '.graphql';
var componentKey = getModuleComponentKey(moduleKey);
var componentField = {
alias: componentKey,
args: [{
kind: 'Argument',
name: JS_FIELD_MODULE_ARG,
type: jsFieldModuleArg.type,
value: {
kind: 'Literal',
loc: (_moduleDirective$args2 = (_moduleDirective$args3 = moduleDirective.args[0]) === null || _moduleDirective$args3 === void 0 ? void 0 : _moduleDirective$args3.loc) !== null && _moduleDirective$args2 !== void 0 ? _moduleDirective$args2 : moduleDirective.loc,
value: moduleName
},
loc: moduleDirective.loc
}, jsFieldIdArg != null ? {
kind: 'Argument',
name: JS_FIELD_ID_ARG,
type: jsFieldIdArg.type,
value: {
kind: 'Literal',
loc: (_moduleDirective$args4 = (_moduleDirective$args5 = moduleDirective.args[0]) === null || _moduleDirective$args5 === void 0 ? void 0 : _moduleDirective$args5.loc) !== null && _moduleDirective$args4 !== void 0 ? _moduleDirective$args4 : moduleDirective.loc,
value: moduleId
},
loc: moduleDirective.loc
} : null].filter(Boolean),
directives: [],
handles: null,
kind: 'ScalarField',
loc: moduleDirective.loc,
metadata: {
skipNormalizationNode: true
},
name: JS_FIELD_NAME,
type: jsModuleType
};
var operationKey = getModuleOperationKey(moduleKey);
var operationField = {
alias: operationKey,
args: [{
kind: 'Argument',
name: JS_FIELD_MODULE_ARG,
type: jsFieldModuleArg.type,
value: {
kind: 'Literal',
loc: moduleDirective.loc,
value: normalizationName
},
loc: moduleDirective.loc
}, jsFieldIdArg != null ? {
kind: 'Argument',
name: JS_FIELD_ID_ARG,
type: jsFieldIdArg.type,
value: {
kind: 'Literal',
loc: (_moduleDirective$args6 = (_moduleDirective$args7 = moduleDirective.args[0]) === null || _moduleDirective$args7 === void 0 ? void 0 : _moduleDirective$args7.loc) !== null && _moduleDirective$args6 !== void 0 ? _moduleDirective$args6 : moduleDirective.loc,
value: moduleId
},
loc: moduleDirective.loc
} : null].filter(Boolean),
directives: [],
handles: null,
kind: 'ScalarField',
loc: moduleDirective.loc,
metadata: {
skipNormalizationNode: true
},
name: JS_FIELD_NAME,
type: jsModuleType
};
return {
kind: 'InlineFragment',
directives: [],
loc: moduleDirective.loc,
metadata: null,
selections: [{
kind: 'ModuleImport',
loc: moduleDirective.loc,
key: moduleKey,
id: moduleId,
module: moduleName,
sourceDocument: documentName,
name: spread.name,
selections: [(0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, spread), {}, {
directives: spread.directives.filter(function (directive) {
return directive !== moduleDirective;
})
}), operationField, componentField]
}],
typeCondition: fragment.type
};
}
module.exports = {
SCHEMA_EXTENSION: SCHEMA_EXTENSION,
transform: matchTransform
};