UNPKG

16.2 kBJavaScriptView Raw
1/**
2 * Copyright (c) Facebook, Inc. and its affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 *
8 * @format
9 */
10'use strict';
11
12var _objectSpread2 = require("@babel/runtime/helpers/interopRequireDefault")(require("@babel/runtime/helpers/objectSpread"));
13
14var _toConsumableArray2 = require("@babel/runtime/helpers/interopRequireDefault")(require("@babel/runtime/helpers/toConsumableArray"));
15
16var CONNECTION = 'connection';
17var HANDLER = 'handler';
18/**
19 * @public
20 *
21 * Transforms fields with the `@connection` directive:
22 * - Verifies that the field type is connection-like.
23 * - Adds a `handle` property to the field, either the user-provided `handle`
24 * argument or the default value "connection".
25 * - Inserts a sub-fragment on the field to ensure that standard connection
26 * fields are fetched (e.g. cursors, node ids, page info).
27 */
28
29function relayConnectionTransform(context) {
30 return require("./GraphQLIRTransformer").transform(context, {
31 Fragment: visitFragmentOrRoot,
32 LinkedField: visitLinkedOrMatchField,
33 MatchField: visitLinkedOrMatchField,
34 Root: visitFragmentOrRoot
35 }, function (node) {
36 return {
37 path: [],
38 connectionMetadata: [],
39 definitionName: node.name
40 };
41 });
42}
43
44var SCHEMA_EXTENSION = 'directive @connection(key: String!, filters: [String], handler: String) on FIELD';
45/**
46 * @internal
47 */
48
49function visitFragmentOrRoot(node, options) {
50 var transformedNode = this.traverse(node, options);
51 var connectionMetadata = options.connectionMetadata;
52
53 if (connectionMetadata.length) {
54 return (0, _objectSpread2["default"])({}, transformedNode, {
55 metadata: (0, _objectSpread2["default"])({}, transformedNode.metadata, {
56 connection: connectionMetadata
57 })
58 });
59 }
60
61 return transformedNode;
62}
63/**
64 * @internal
65 */
66
67
68function visitLinkedOrMatchField(field, options) {
69 var _handler;
70
71 var isPlural = require("./GraphQLSchemaUtils").getNullableType(field.type) instanceof require("graphql").GraphQLList;
72
73 options.path.push(isPlural ? null : field.alias || field.name);
74 var transformedField = this.traverse(field, options);
75 var connectionDirective = field.directives.find(function (directive) {
76 return directive.name === CONNECTION;
77 });
78
79 if (!connectionDirective) {
80 options.path.pop();
81 return transformedField;
82 }
83
84 var definitionName = options.definitionName;
85 validateConnectionSelection(definitionName, transformedField);
86 validateConnectionType(definitionName, transformedField);
87 var pathHasPlural = options.path.includes(null);
88 var firstArg = findArg(transformedField, require("./RelayConnectionConstants").FIRST);
89 var lastArg = findArg(transformedField, require("./RelayConnectionConstants").LAST);
90 var direction = null;
91 var countArg = null;
92 var cursorArg = null;
93
94 if (firstArg && !lastArg) {
95 direction = 'forward';
96 countArg = firstArg;
97 cursorArg = findArg(transformedField, require("./RelayConnectionConstants").AFTER);
98 } else if (lastArg && !firstArg) {
99 direction = 'backward';
100 countArg = lastArg;
101 cursorArg = findArg(transformedField, require("./RelayConnectionConstants").BEFORE);
102 } else if (lastArg && firstArg) {
103 direction = 'bidirectional'; // TODO(T26511885) Maybe add connection metadata to this case
104 }
105
106 var countVariable = countArg && countArg.value.kind === 'Variable' ? countArg.value.variableName : null;
107 var cursorVariable = cursorArg && cursorArg.value.kind === 'Variable' ? cursorArg.value.variableName : null;
108 options.connectionMetadata.push({
109 count: countVariable,
110 cursor: cursorVariable,
111 direction: direction,
112 path: pathHasPlural ? null : (0, _toConsumableArray2["default"])(options.path)
113 });
114 options.path.pop();
115
116 var _getLiteralArgumentVa = require("./getLiteralArgumentValues")(connectionDirective.args),
117 handler = _getLiteralArgumentVa.handler,
118 key = _getLiteralArgumentVa.key,
119 filters = _getLiteralArgumentVa.filters;
120
121 if (handler != null && typeof handler !== 'string') {
122 var _ref, _handleArg$value;
123
124 var handleArg = connectionDirective.args.find(function (arg) {
125 return arg.name === 'key';
126 });
127 throw require("./RelayCompilerError").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]);
128 }
129
130 if (typeof key !== 'string') {
131 var _ref2, _keyArg$value;
132
133 var keyArg = connectionDirective.args.find(function (arg) {
134 return arg.name === 'key';
135 });
136 throw require("./RelayCompilerError").createUserError("Expected the ".concat(require("./RelayConnectionConstants").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]);
137 }
138
139 var postfix = field.alias || field.name;
140
141 if (!key.endsWith('_' + postfix)) {
142 var _ref3, _keyArg$value2;
143
144 var _keyArg = connectionDirective.args.find(function (arg) {
145 return arg.name === 'key';
146 });
147
148 throw require("./RelayCompilerError").createUserError("Expected the ".concat(require("./RelayConnectionConstants").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]);
149 }
150
151 var generateFilters = function generateFilters() {
152 var filteredVariableArgs = field.args.filter(function (arg) {
153 return !require("relay-runtime").ConnectionInterface.isConnectionCall({
154 name: arg.name,
155 value: null
156 });
157 }).map(function (arg) {
158 return arg.name;
159 });
160 return filteredVariableArgs.length === 0 ? null : filteredVariableArgs;
161 };
162
163 var handle = {
164 name: (_handler = handler) !== null && _handler !== void 0 ? _handler : CONNECTION,
165 key: key,
166 filters: filters || generateFilters()
167 };
168
169 if (direction !== null) {
170 var fragment = generateConnectionFragment(this.getContext(), transformedField.loc, transformedField.type, direction);
171 transformedField = (0, _objectSpread2["default"])({}, transformedField, {
172 selections: transformedField.selections.concat(fragment)
173 });
174 }
175
176 return (0, _objectSpread2["default"])({}, transformedField, {
177 directives: transformedField.directives.filter(function (directive) {
178 return directive.name !== CONNECTION;
179 }),
180 handles: transformedField.handles ? (0, _toConsumableArray2["default"])(transformedField.handles).concat([handle]) : [handle]
181 });
182}
183/**
184 * @internal
185 *
186 * Generates a fragment on the given type that fetches the minimal connection
187 * fields in order to merge different pagination results together at runtime.
188 */
189
190
191function generateConnectionFragment(context, loc, type, direction) {
192 var _ConnectionInterface$ = require("relay-runtime").ConnectionInterface.get(),
193 CURSOR = _ConnectionInterface$.CURSOR,
194 EDGES = _ConnectionInterface$.EDGES,
195 END_CURSOR = _ConnectionInterface$.END_CURSOR,
196 HAS_NEXT_PAGE = _ConnectionInterface$.HAS_NEXT_PAGE,
197 HAS_PREV_PAGE = _ConnectionInterface$.HAS_PREV_PAGE,
198 NODE = _ConnectionInterface$.NODE,
199 PAGE_INFO = _ConnectionInterface$.PAGE_INFO,
200 START_CURSOR = _ConnectionInterface$.START_CURSOR;
201
202 var compositeType = require("graphql").assertCompositeType(require("./GraphQLSchemaUtils").getNullableType(type));
203
204 var pageInfo = PAGE_INFO;
205
206 if (direction === 'forward') {
207 pageInfo += "{\n ".concat(END_CURSOR, "\n ").concat(HAS_NEXT_PAGE, "\n }");
208 } else if (direction === 'backward') {
209 pageInfo += "{\n ".concat(HAS_PREV_PAGE, "\n ").concat(START_CURSOR, "\n }");
210 } else {
211 pageInfo += "{\n ".concat(END_CURSOR, "\n ").concat(HAS_NEXT_PAGE, "\n ").concat(HAS_PREV_PAGE, "\n ").concat(START_CURSOR, "\n }");
212 }
213
214 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 }");
215
216 var ast = require("graphql").parse(fragmentString);
217
218 var fragmentAST = ast.definitions[0];
219
220 if (fragmentAST == null || fragmentAST.kind !== 'FragmentDefinition') {
221 throw require("./RelayCompilerError").createCompilerError('RelayConnectionTransform: Expected a fragment definition AST.', null, [fragmentAST].filter(Boolean));
222 }
223
224 var fragment = require("./RelayParser").transform(context.clientSchema, [fragmentAST])[0];
225
226 if (fragment == null || fragment.kind !== 'Fragment') {
227 throw require("./RelayCompilerError").createCompilerError('RelayConnectionTransform: Expected a connection fragment.', [fragment === null || fragment === void 0 ? void 0 : fragment.loc].filter(Boolean));
228 }
229
230 return {
231 directives: [],
232 kind: 'InlineFragment',
233 loc: {
234 kind: 'Derived',
235 source: loc
236 },
237 metadata: null,
238 selections: fragment.selections,
239 typeCondition: compositeType
240 };
241}
242
243function findArg(field, argName) {
244 return field.args && field.args.find(function (arg) {
245 return arg.name === argName;
246 });
247}
248/**
249 * @internal
250 *
251 * Validates that the selection is a valid connection:
252 * - Specifies a first or last argument to prevent accidental, unconstrained
253 * data access.
254 * - Has an `edges` selection, otherwise there is nothing to paginate.
255 *
256 * TODO: This implementation requires the edges field to be a direct selection
257 * and not contained within an inline fragment or fragment spread. It's
258 * technically possible to remove this restriction if this pattern becomes
259 * common/necessary.
260 */
261
262
263function validateConnectionSelection(definitionName, field) {
264 var _ConnectionInterface$2 = require("relay-runtime").ConnectionInterface.get(),
265 EDGES = _ConnectionInterface$2.EDGES;
266
267 if (!findArg(field, require("./RelayConnectionConstants").FIRST) && !findArg(field, require("./RelayConnectionConstants").LAST)) {
268 throw require("./RelayCompilerError").createUserError("Expected field `".concat(field.name, ": ") + "".concat(String(field.type), "` to have a ").concat(require("./RelayConnectionConstants").FIRST, " or ").concat(require("./RelayConnectionConstants").LAST, " argument in ") + "document `".concat(definitionName, "`."), [field.loc]);
269 }
270
271 if (!field.selections.some(function (selection) {
272 return selection.kind === 'LinkedField' && selection.name === EDGES;
273 })) {
274 throw require("./RelayCompilerError").createUserError("Expected field `".concat(field.name, ": ") + "".concat(String(field.type), "` to have a ").concat(EDGES, " selection in document ") + "`".concat(definitionName, "`."), [field.loc]);
275 }
276}
277/**
278 * @internal
279 *
280 * Validates that the type satisfies the Connection specification:
281 * - The type has an edges field, and edges have scalar `cursor` and object
282 * `node` fields.
283 * - The type has a page info field which is an object with the correct
284 * subfields.
285 */
286
287
288function validateConnectionType(definitionName, field) {
289 var type = field.type;
290
291 var _ConnectionInterface$3 = require("relay-runtime").ConnectionInterface.get(),
292 CURSOR = _ConnectionInterface$3.CURSOR,
293 EDGES = _ConnectionInterface$3.EDGES,
294 END_CURSOR = _ConnectionInterface$3.END_CURSOR,
295 HAS_NEXT_PAGE = _ConnectionInterface$3.HAS_NEXT_PAGE,
296 HAS_PREV_PAGE = _ConnectionInterface$3.HAS_PREV_PAGE,
297 NODE = _ConnectionInterface$3.NODE,
298 PAGE_INFO = _ConnectionInterface$3.PAGE_INFO,
299 START_CURSOR = _ConnectionInterface$3.START_CURSOR;
300
301 var typeWithFields = require("./GraphQLSchemaUtils").assertTypeWithFields(require("./GraphQLSchemaUtils").getNullableType(type));
302
303 var typeFields = typeWithFields.getFields();
304 var edges = typeFields[EDGES];
305
306 if (edges == null) {
307 throw require("./RelayCompilerError").createUserError("Expected type '".concat(String(type), "' to have an '").concat(EDGES, "' field in document '").concat(definitionName, "'."), [field.loc]);
308 }
309
310 var edgesType = require("./GraphQLSchemaUtils").getNullableType(edges.type);
311
312 if (!(edgesType instanceof require("graphql").GraphQLList)) {
313 throw require("./RelayCompilerError").createUserError("Expected '".concat(EDGES, "' field on type '").concat(String(type), "' to be a list type in document '").concat(definitionName, "'."), [field.loc]);
314 }
315
316 var edgeType = require("./GraphQLSchemaUtils").getNullableType(edgesType.ofType);
317
318 if (!(edgeType instanceof require("graphql").GraphQLObjectType)) {
319 throw require("./RelayCompilerError").createUserError("Expected '".concat(EDGES, "' field on type '").concat(String(type), "' to be a list of objects in document '").concat(definitionName, "'."), [field.loc]);
320 }
321
322 var node = edgeType.getFields()[NODE];
323
324 if (node == null) {
325 throw require("./RelayCompilerError").createUserError("Expected type '".concat(String(type), "' to have have a '").concat(EDGES, " { ").concat(NODE, " }' field in in document '").concat(definitionName, "'."), [field.loc]);
326 }
327
328 var nodeType = require("./GraphQLSchemaUtils").getNullableType(node.type);
329
330 if (!(nodeType instanceof require("graphql").GraphQLInterfaceType || nodeType instanceof require("graphql").GraphQLUnionType || nodeType instanceof require("graphql").GraphQLObjectType)) {
331 throw require("./RelayCompilerError").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]);
332 }
333
334 var cursor = edgeType.getFields()[CURSOR];
335
336 if (cursor == null || !(require("./GraphQLSchemaUtils").getNullableType(cursor.type) instanceof require("graphql").GraphQLScalarType)) {
337 throw require("./RelayCompilerError").createUserError("Expected type '".concat(String(type), "' to have a '").concat(EDGES, " { ").concat(CURSOR, " }' scalar field in document '").concat(definitionName, "'."), [field.loc]);
338 }
339
340 var pageInfo = typeFields[PAGE_INFO];
341
342 if (pageInfo == null) {
343 throw require("./RelayCompilerError").createUserError("Expected type '".concat(String(type), "' to have a '").concat(EDGES, " { ").concat(PAGE_INFO, " }' field in document '").concat(definitionName, "'."), [field.loc]);
344 }
345
346 var pageInfoType = require("./GraphQLSchemaUtils").getNullableType(pageInfo.type);
347
348 if (!(pageInfoType instanceof require("graphql").GraphQLObjectType)) {
349 throw require("./RelayCompilerError").createUserError("Expected type '".concat(String(type), "' to have a '").concat(EDGES, " { ").concat(PAGE_INFO, " }' field with object type in document '").concat(definitionName, "'."), [field.loc]);
350 }
351
352 [END_CURSOR, HAS_NEXT_PAGE, HAS_PREV_PAGE, START_CURSOR].forEach(function (fieldName) {
353 var pageInfoField = pageInfoType.getFields()[fieldName];
354
355 if (pageInfoField == null || !(require("./GraphQLSchemaUtils").getNullableType(pageInfoField.type) instanceof require("graphql").GraphQLScalarType)) {
356 throw require("./RelayCompilerError").createUserError("Expected type '".concat(String(pageInfo.type), "' to have a '").concat(fieldName, "' scalar field in document '").concat(definitionName, "'."), [field.loc]);
357 }
358 });
359}
360
361module.exports = {
362 CONNECTION: CONNECTION,
363 SCHEMA_EXTENSION: SCHEMA_EXTENSION,
364 transform: relayConnectionTransform
365};
\No newline at end of file