393 lines
16 KiB
JavaScript
393 lines
16 KiB
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
|
|
*/
|
|
'use strict';
|
|
|
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
|
|
var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread"));
|
|
|
|
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
|
|
|
|
var IRTransformer = require("./GraphQLIRTransformer");
|
|
|
|
var RelayParser = require("./RelayParser");
|
|
|
|
var SchemaUtils = require("./GraphQLSchemaUtils");
|
|
|
|
var getLiteralArgumentValues = require("./getLiteralArgumentValues");
|
|
|
|
var _require = require("./RelayCompilerError"),
|
|
createCompilerError = _require.createCompilerError,
|
|
createUserError = _require.createUserError;
|
|
|
|
var _require2 = require("./RelayConnectionConstants"),
|
|
AFTER = _require2.AFTER,
|
|
BEFORE = _require2.BEFORE,
|
|
FIRST = _require2.FIRST,
|
|
KEY = _require2.KEY,
|
|
LAST = _require2.LAST;
|
|
|
|
var _require3 = require("graphql"),
|
|
assertCompositeType = _require3.assertCompositeType,
|
|
GraphQLInterfaceType = _require3.GraphQLInterfaceType,
|
|
GraphQLList = _require3.GraphQLList,
|
|
GraphQLObjectType = _require3.GraphQLObjectType,
|
|
GraphQLScalarType = _require3.GraphQLScalarType,
|
|
GraphQLUnionType = _require3.GraphQLUnionType,
|
|
parse = _require3.parse;
|
|
|
|
var _require4 = require("relay-runtime"),
|
|
ConnectionInterface = _require4.ConnectionInterface;
|
|
|
|
var CONNECTION = 'connection';
|
|
var HANDLER = 'handler';
|
|
/**
|
|
* @public
|
|
*
|
|
* Transforms fields with the `@connection` directive:
|
|
* - Verifies that the field type is connection-like.
|
|
* - Adds a `handle` property to the field, either the user-provided `handle`
|
|
* argument or the default value "connection".
|
|
* - Inserts a sub-fragment on the field to ensure that standard connection
|
|
* fields are fetched (e.g. cursors, node ids, page info).
|
|
*/
|
|
|
|
function relayConnectionTransform(context) {
|
|
return IRTransformer.transform(context, {
|
|
Fragment: visitFragmentOrRoot,
|
|
LinkedField: visitLinkedOrMatchField,
|
|
MatchField: visitLinkedOrMatchField,
|
|
Root: visitFragmentOrRoot
|
|
}, function (node) {
|
|
return {
|
|
path: [],
|
|
connectionMetadata: [],
|
|
definitionName: node.name
|
|
};
|
|
});
|
|
}
|
|
|
|
var SCHEMA_EXTENSION = 'directive @connection(key: String!, filters: [String], handler: String) on FIELD';
|
|
/**
|
|
* @internal
|
|
*/
|
|
|
|
function visitFragmentOrRoot(node, options) {
|
|
var transformedNode = this.traverse(node, options);
|
|
var connectionMetadata = options.connectionMetadata;
|
|
|
|
if (connectionMetadata.length) {
|
|
return (0, _objectSpread2["default"])({}, transformedNode, {
|
|
metadata: (0, _objectSpread2["default"])({}, transformedNode.metadata, {
|
|
connection: connectionMetadata
|
|
})
|
|
});
|
|
}
|
|
|
|
return transformedNode;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
|
|
|
|
function visitLinkedOrMatchField(field, options) {
|
|
var _handler;
|
|
|
|
var isPlural = SchemaUtils.getNullableType(field.type) instanceof GraphQLList;
|
|
options.path.push(isPlural ? null : field.alias || field.name);
|
|
var transformedField = this.traverse(field, options);
|
|
var connectionDirective = field.directives.find(function (directive) {
|
|
return directive.name === CONNECTION;
|
|
});
|
|
|
|
if (!connectionDirective) {
|
|
options.path.pop();
|
|
return transformedField;
|
|
}
|
|
|
|
var definitionName = options.definitionName;
|
|
validateConnectionSelection(definitionName, transformedField);
|
|
validateConnectionType(definitionName, transformedField);
|
|
var pathHasPlural = options.path.includes(null);
|
|
var firstArg = findArg(transformedField, FIRST);
|
|
var lastArg = findArg(transformedField, LAST);
|
|
var direction = null;
|
|
var countArg = null;
|
|
var cursorArg = null;
|
|
|
|
if (firstArg && !lastArg) {
|
|
direction = 'forward';
|
|
countArg = firstArg;
|
|
cursorArg = findArg(transformedField, AFTER);
|
|
} else if (lastArg && !firstArg) {
|
|
direction = 'backward';
|
|
countArg = lastArg;
|
|
cursorArg = findArg(transformedField, BEFORE);
|
|
} else if (lastArg && firstArg) {
|
|
direction = 'bidirectional'; // TODO(T26511885) Maybe add connection metadata to this case
|
|
}
|
|
|
|
var countVariable = countArg && countArg.value.kind === 'Variable' ? countArg.value.variableName : null;
|
|
var cursorVariable = cursorArg && cursorArg.value.kind === 'Variable' ? cursorArg.value.variableName : null;
|
|
options.connectionMetadata.push({
|
|
count: countVariable,
|
|
cursor: cursorVariable,
|
|
direction: direction,
|
|
path: pathHasPlural ? null : (0, _toConsumableArray2["default"])(options.path)
|
|
});
|
|
options.path.pop();
|
|
|
|
var _getLiteralArgumentVa = getLiteralArgumentValues(connectionDirective.args),
|
|
handler = _getLiteralArgumentVa.handler,
|
|
key = _getLiteralArgumentVa.key,
|
|
filters = _getLiteralArgumentVa.filters;
|
|
|
|
if (handler != null && typeof handler !== 'string') {
|
|
var _ref, _handleArg$value;
|
|
|
|
var handleArg = connectionDirective.args.find(function (arg) {
|
|
return arg.name === 'key';
|
|
});
|
|
throw createUserError("Expected the ".concat(HANDLER, " argument to ") + "@".concat(CONNECTION, " to be a string literal for field ").concat(field.name, "."), [(_ref = handleArg === null || handleArg === void 0 ? void 0 : (_handleArg$value = handleArg.value) === null || _handleArg$value === void 0 ? void 0 : _handleArg$value.loc) !== null && _ref !== void 0 ? _ref : connectionDirective.loc]);
|
|
}
|
|
|
|
if (typeof key !== 'string') {
|
|
var _ref2, _keyArg$value;
|
|
|
|
var keyArg = connectionDirective.args.find(function (arg) {
|
|
return arg.name === 'key';
|
|
});
|
|
throw createUserError("Expected the ".concat(KEY, " argument to ") + "@".concat(CONNECTION, " to be a string literal for field ").concat(field.name, "."), [(_ref2 = keyArg === null || keyArg === void 0 ? void 0 : (_keyArg$value = keyArg.value) === null || _keyArg$value === void 0 ? void 0 : _keyArg$value.loc) !== null && _ref2 !== void 0 ? _ref2 : connectionDirective.loc]);
|
|
}
|
|
|
|
var postfix = field.alias || field.name;
|
|
|
|
if (!key.endsWith('_' + postfix)) {
|
|
var _ref3, _keyArg$value2;
|
|
|
|
var _keyArg = connectionDirective.args.find(function (arg) {
|
|
return arg.name === 'key';
|
|
});
|
|
|
|
throw createUserError("Expected the ".concat(KEY, " argument to ") + "@".concat(CONNECTION, " to be of form <SomeName>_").concat(postfix, ", got '").concat(key, "'. ") + 'For detailed explanation, check out ' + 'https://facebook.github.io/relay/docs/en/pagination-container.html#connection', [(_ref3 = _keyArg === null || _keyArg === void 0 ? void 0 : (_keyArg$value2 = _keyArg.value) === null || _keyArg$value2 === void 0 ? void 0 : _keyArg$value2.loc) !== null && _ref3 !== void 0 ? _ref3 : connectionDirective.loc]);
|
|
}
|
|
|
|
var generateFilters = function generateFilters() {
|
|
var filteredVariableArgs = field.args.filter(function (arg) {
|
|
return !ConnectionInterface.isConnectionCall({
|
|
name: arg.name,
|
|
value: null
|
|
});
|
|
}).map(function (arg) {
|
|
return arg.name;
|
|
});
|
|
return filteredVariableArgs.length === 0 ? null : filteredVariableArgs;
|
|
};
|
|
|
|
var handle = {
|
|
name: (_handler = handler) !== null && _handler !== void 0 ? _handler : CONNECTION,
|
|
key: key,
|
|
filters: filters || generateFilters()
|
|
};
|
|
|
|
if (direction !== null) {
|
|
var fragment = generateConnectionFragment(this.getContext(), transformedField.loc, transformedField.type, direction);
|
|
transformedField = (0, _objectSpread2["default"])({}, transformedField, {
|
|
selections: transformedField.selections.concat(fragment)
|
|
});
|
|
}
|
|
|
|
return (0, _objectSpread2["default"])({}, transformedField, {
|
|
directives: transformedField.directives.filter(function (directive) {
|
|
return directive.name !== CONNECTION;
|
|
}),
|
|
handles: transformedField.handles ? (0, _toConsumableArray2["default"])(transformedField.handles).concat([handle]) : [handle]
|
|
});
|
|
}
|
|
/**
|
|
* @internal
|
|
*
|
|
* Generates a fragment on the given type that fetches the minimal connection
|
|
* fields in order to merge different pagination results together at runtime.
|
|
*/
|
|
|
|
|
|
function generateConnectionFragment(context, loc, type, direction) {
|
|
var _ConnectionInterface$ = ConnectionInterface.get(),
|
|
CURSOR = _ConnectionInterface$.CURSOR,
|
|
EDGES = _ConnectionInterface$.EDGES,
|
|
END_CURSOR = _ConnectionInterface$.END_CURSOR,
|
|
HAS_NEXT_PAGE = _ConnectionInterface$.HAS_NEXT_PAGE,
|
|
HAS_PREV_PAGE = _ConnectionInterface$.HAS_PREV_PAGE,
|
|
NODE = _ConnectionInterface$.NODE,
|
|
PAGE_INFO = _ConnectionInterface$.PAGE_INFO,
|
|
START_CURSOR = _ConnectionInterface$.START_CURSOR;
|
|
|
|
var compositeType = assertCompositeType(SchemaUtils.getNullableType(type));
|
|
var pageInfo = PAGE_INFO;
|
|
|
|
if (direction === 'forward') {
|
|
pageInfo += "{\n ".concat(END_CURSOR, "\n ").concat(HAS_NEXT_PAGE, "\n }");
|
|
} else if (direction === 'backward') {
|
|
pageInfo += "{\n ".concat(HAS_PREV_PAGE, "\n ").concat(START_CURSOR, "\n }");
|
|
} else {
|
|
pageInfo += "{\n ".concat(END_CURSOR, "\n ").concat(HAS_NEXT_PAGE, "\n ").concat(HAS_PREV_PAGE, "\n ").concat(START_CURSOR, "\n }");
|
|
}
|
|
|
|
var fragmentString = "fragment ConnectionFragment on ".concat(String(compositeType), " {\n ").concat(EDGES, " {\n ").concat(CURSOR, "\n ").concat(NODE, " {\n __typename # rely on GenerateRequisiteFieldTransform to add \"id\"\n }\n }\n ").concat(pageInfo, "\n }");
|
|
var ast = parse(fragmentString);
|
|
var fragmentAST = ast.definitions[0];
|
|
|
|
if (fragmentAST == null || fragmentAST.kind !== 'FragmentDefinition') {
|
|
throw createCompilerError('RelayConnectionTransform: Expected a fragment definition AST.', null, [fragmentAST].filter(Boolean));
|
|
}
|
|
|
|
var fragment = RelayParser.transform(context.clientSchema, [fragmentAST])[0];
|
|
|
|
if (fragment == null || fragment.kind !== 'Fragment') {
|
|
throw createCompilerError('RelayConnectionTransform: Expected a connection fragment.', [fragment === null || fragment === void 0 ? void 0 : fragment.loc].filter(Boolean));
|
|
}
|
|
|
|
return {
|
|
directives: [],
|
|
kind: 'InlineFragment',
|
|
loc: {
|
|
kind: 'Derived',
|
|
source: loc
|
|
},
|
|
metadata: null,
|
|
selections: fragment.selections,
|
|
typeCondition: compositeType
|
|
};
|
|
}
|
|
|
|
function findArg(field, argName) {
|
|
return field.args && field.args.find(function (arg) {
|
|
return arg.name === argName;
|
|
});
|
|
}
|
|
/**
|
|
* @internal
|
|
*
|
|
* Validates that the selection is a valid connection:
|
|
* - Specifies a first or last argument to prevent accidental, unconstrained
|
|
* data access.
|
|
* - Has an `edges` selection, otherwise there is nothing to paginate.
|
|
*
|
|
* TODO: This implementation requires the edges field to be a direct selection
|
|
* and not contained within an inline fragment or fragment spread. It's
|
|
* technically possible to remove this restriction if this pattern becomes
|
|
* common/necessary.
|
|
*/
|
|
|
|
|
|
function validateConnectionSelection(definitionName, field) {
|
|
var _ConnectionInterface$2 = ConnectionInterface.get(),
|
|
EDGES = _ConnectionInterface$2.EDGES;
|
|
|
|
if (!findArg(field, FIRST) && !findArg(field, LAST)) {
|
|
throw createUserError("Expected field `".concat(field.name, ": ") + "".concat(String(field.type), "` to have a ").concat(FIRST, " or ").concat(LAST, " argument in ") + "document `".concat(definitionName, "`."), [field.loc]);
|
|
}
|
|
|
|
if (!field.selections.some(function (selection) {
|
|
return selection.kind === 'LinkedField' && selection.name === EDGES;
|
|
})) {
|
|
throw createUserError("Expected field `".concat(field.name, ": ") + "".concat(String(field.type), "` to have a ").concat(EDGES, " selection in document ") + "`".concat(definitionName, "`."), [field.loc]);
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*
|
|
* Validates that the type satisfies the Connection specification:
|
|
* - The type has an edges field, and edges have scalar `cursor` and object
|
|
* `node` fields.
|
|
* - The type has a page info field which is an object with the correct
|
|
* subfields.
|
|
*/
|
|
|
|
|
|
function validateConnectionType(definitionName, field) {
|
|
var type = field.type;
|
|
|
|
var _ConnectionInterface$3 = ConnectionInterface.get(),
|
|
CURSOR = _ConnectionInterface$3.CURSOR,
|
|
EDGES = _ConnectionInterface$3.EDGES,
|
|
END_CURSOR = _ConnectionInterface$3.END_CURSOR,
|
|
HAS_NEXT_PAGE = _ConnectionInterface$3.HAS_NEXT_PAGE,
|
|
HAS_PREV_PAGE = _ConnectionInterface$3.HAS_PREV_PAGE,
|
|
NODE = _ConnectionInterface$3.NODE,
|
|
PAGE_INFO = _ConnectionInterface$3.PAGE_INFO,
|
|
START_CURSOR = _ConnectionInterface$3.START_CURSOR;
|
|
|
|
var typeWithFields = SchemaUtils.assertTypeWithFields(SchemaUtils.getNullableType(type));
|
|
var typeFields = typeWithFields.getFields();
|
|
var edges = typeFields[EDGES];
|
|
|
|
if (edges == null) {
|
|
throw createUserError("Expected type '".concat(String(type), "' to have an '").concat(EDGES, "' field in document '").concat(definitionName, "'."), [field.loc]);
|
|
}
|
|
|
|
var edgesType = SchemaUtils.getNullableType(edges.type);
|
|
|
|
if (!(edgesType instanceof GraphQLList)) {
|
|
throw createUserError("Expected '".concat(EDGES, "' field on type '").concat(String(type), "' to be a list type in document '").concat(definitionName, "'."), [field.loc]);
|
|
}
|
|
|
|
var edgeType = SchemaUtils.getNullableType(edgesType.ofType);
|
|
|
|
if (!(edgeType instanceof GraphQLObjectType)) {
|
|
throw createUserError("Expected '".concat(EDGES, "' field on type '").concat(String(type), "' to be a list of objects in document '").concat(definitionName, "'."), [field.loc]);
|
|
}
|
|
|
|
var node = edgeType.getFields()[NODE];
|
|
|
|
if (node == null) {
|
|
throw createUserError("Expected type '".concat(String(type), "' to have have a '").concat(EDGES, " { ").concat(NODE, " }' field in in document '").concat(definitionName, "'."), [field.loc]);
|
|
}
|
|
|
|
var nodeType = SchemaUtils.getNullableType(node.type);
|
|
|
|
if (!(nodeType instanceof GraphQLInterfaceType || nodeType instanceof GraphQLUnionType || nodeType instanceof GraphQLObjectType)) {
|
|
throw createUserError("Expected type '".concat(String(type), "' to have a '").concat(EDGES, " { ").concat(NODE, " }' field for which the type is an interface, object, or union in document '").concat(definitionName, "'."), [field.loc]);
|
|
}
|
|
|
|
var cursor = edgeType.getFields()[CURSOR];
|
|
|
|
if (cursor == null || !(SchemaUtils.getNullableType(cursor.type) instanceof GraphQLScalarType)) {
|
|
throw createUserError("Expected type '".concat(String(type), "' to have a '").concat(EDGES, " { ").concat(CURSOR, " }' scalar field in document '").concat(definitionName, "'."), [field.loc]);
|
|
}
|
|
|
|
var pageInfo = typeFields[PAGE_INFO];
|
|
|
|
if (pageInfo == null) {
|
|
throw createUserError("Expected type '".concat(String(type), "' to have a '").concat(EDGES, " { ").concat(PAGE_INFO, " }' field in document '").concat(definitionName, "'."), [field.loc]);
|
|
}
|
|
|
|
var pageInfoType = SchemaUtils.getNullableType(pageInfo.type);
|
|
|
|
if (!(pageInfoType instanceof GraphQLObjectType)) {
|
|
throw createUserError("Expected type '".concat(String(type), "' to have a '").concat(EDGES, " { ").concat(PAGE_INFO, " }' field with object type in document '").concat(definitionName, "'."), [field.loc]);
|
|
}
|
|
|
|
[END_CURSOR, HAS_NEXT_PAGE, HAS_PREV_PAGE, START_CURSOR].forEach(function (fieldName) {
|
|
var pageInfoField = pageInfoType.getFields()[fieldName];
|
|
|
|
if (pageInfoField == null || !(SchemaUtils.getNullableType(pageInfoField.type) instanceof GraphQLScalarType)) {
|
|
throw createUserError("Expected type '".concat(String(pageInfo.type), "' to have a '").concat(fieldName, "' scalar field in document '").concat(definitionName, "'."), [field.loc]);
|
|
}
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
CONNECTION: CONNECTION,
|
|
SCHEMA_EXTENSION: SCHEMA_EXTENSION,
|
|
transform: relayConnectionTransform
|
|
}; |