UNPKG

10.5 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.ApolloFederation = exports.removeFederation = exports.addFederationReferencesToSchema = exports.federationSpec = void 0;
4const tslib_1 = require("tslib");
5const graphql_1 = require("graphql");
6const merge_js_1 = tslib_1.__importDefault(require("lodash/merge.js"));
7const utils_js_1 = require("./utils.js");
8const utils_1 = require("@graphql-tools/utils");
9const index_js_1 = require("./index.js");
10/**
11 * Federation Spec
12 */
13exports.federationSpec = (0, graphql_1.parse)(/* GraphQL */ `
14 scalar _FieldSet
15
16 directive @external on FIELD_DEFINITION
17 directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
18 directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
19 directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
20`);
21/**
22 * Adds `__resolveReference` in each ObjectType involved in Federation.
23 * @param schema
24 */
25function addFederationReferencesToSchema(schema) {
26 return (0, utils_1.mapSchema)(schema, {
27 [utils_1.MapperKind.OBJECT_TYPE]: type => {
28 if (isFederationObjectType(type, schema)) {
29 const typeConfig = type.toConfig();
30 typeConfig.fields = {
31 [resolveReferenceFieldName]: {
32 type,
33 },
34 ...typeConfig.fields,
35 };
36 return new graphql_1.GraphQLObjectType(typeConfig);
37 }
38 return type;
39 },
40 });
41}
42exports.addFederationReferencesToSchema = addFederationReferencesToSchema;
43/**
44 * Removes Federation Spec from GraphQL Schema
45 * @param schema
46 * @param config
47 */
48function removeFederation(schema) {
49 return (0, utils_1.mapSchema)(schema, {
50 [utils_1.MapperKind.QUERY]: queryType => {
51 const queryTypeConfig = queryType.toConfig();
52 delete queryTypeConfig.fields._entities;
53 delete queryTypeConfig.fields._service;
54 return new graphql_1.GraphQLObjectType(queryTypeConfig);
55 },
56 [utils_1.MapperKind.UNION_TYPE]: unionType => {
57 const unionTypeName = unionType.name;
58 if (unionTypeName === '_Entity' || unionTypeName === '_Any') {
59 return null;
60 }
61 return unionType;
62 },
63 [utils_1.MapperKind.OBJECT_TYPE]: objectType => {
64 if (objectType.name === '_Service') {
65 return null;
66 }
67 return objectType;
68 },
69 });
70}
71exports.removeFederation = removeFederation;
72const resolveReferenceFieldName = '__resolveReference';
73class ApolloFederation {
74 constructor({ enabled, schema }) {
75 this.enabled = false;
76 this.enabled = enabled;
77 this.schema = schema;
78 this.providesMap = this.createMapOfProvides();
79 }
80 /**
81 * Excludes types definde by Federation
82 * @param typeNames List of type names
83 */
84 filterTypeNames(typeNames) {
85 return this.enabled ? typeNames.filter(t => t !== '_FieldSet') : typeNames;
86 }
87 /**
88 * Excludes `__resolveReference` fields
89 * @param fieldNames List of field names
90 */
91 filterFieldNames(fieldNames) {
92 return this.enabled ? fieldNames.filter(t => t !== resolveReferenceFieldName) : fieldNames;
93 }
94 /**
95 * Decides if directive should not be generated
96 * @param name directive's name
97 */
98 skipDirective(name) {
99 return this.enabled && ['external', 'requires', 'provides', 'key'].includes(name);
100 }
101 /**
102 * Decides if scalar should not be generated
103 * @param name directive's name
104 */
105 skipScalar(name) {
106 return this.enabled && name === '_FieldSet';
107 }
108 /**
109 * Decides if field should not be generated
110 * @param data
111 */
112 skipField({ fieldNode, parentType }) {
113 if (!this.enabled || !(0, graphql_1.isObjectType)(parentType) || !isFederationObjectType(parentType, this.schema)) {
114 return false;
115 }
116 return this.isExternalAndNotProvided(fieldNode, parentType);
117 }
118 isResolveReferenceField(fieldNode) {
119 const name = typeof fieldNode.name === 'string' ? fieldNode.name : fieldNode.name.value;
120 return this.enabled && name === resolveReferenceFieldName;
121 }
122 /**
123 * Transforms ParentType signature in ObjectTypes involved in Federation
124 * @param data
125 */
126 transformParentType({ fieldNode, parentType, parentTypeSignature, }) {
127 if (this.enabled &&
128 (0, graphql_1.isObjectType)(parentType) &&
129 isFederationObjectType(parentType, this.schema) &&
130 (isTypeExtension(parentType, this.schema) || fieldNode.name.value === resolveReferenceFieldName)) {
131 const keys = getDirectivesByName('key', parentType);
132 if (keys.length) {
133 const outputs = [`{ __typename: '${parentType.name}' } &`];
134 // Look for @requires and see what the service needs and gets
135 const requires = getDirectivesByName('requires', fieldNode).map(this.extractKeyOrRequiresFieldSet);
136 const requiredFields = this.translateFieldSet((0, merge_js_1.default)({}, ...requires), parentTypeSignature);
137 // @key() @key() - "primary keys" in Federation
138 const primaryKeys = keys.map(def => {
139 const fields = this.extractKeyOrRequiresFieldSet(def);
140 return this.translateFieldSet(fields, parentTypeSignature);
141 });
142 const [open, close] = primaryKeys.length > 1 ? ['(', ')'] : ['', ''];
143 outputs.push([open, primaryKeys.join(' | '), close].join(''));
144 // include required fields
145 if (requires.length) {
146 outputs.push(`& ${requiredFields}`);
147 }
148 return outputs.join(' ');
149 }
150 }
151 return parentTypeSignature;
152 }
153 isExternalAndNotProvided(fieldNode, objectType) {
154 return this.isExternal(fieldNode) && !this.hasProvides(objectType, fieldNode);
155 }
156 isExternal(node) {
157 return getDirectivesByName('external', node).length > 0;
158 }
159 hasProvides(objectType, node) {
160 const fields = this.providesMap[(0, graphql_1.isObjectType)(objectType) ? objectType.name : objectType.name.value];
161 if (fields && fields.length) {
162 return fields.includes(node.name.value);
163 }
164 return false;
165 }
166 translateFieldSet(fields, parentTypeRef) {
167 return `GraphQLRecursivePick<${parentTypeRef}, ${JSON.stringify(fields)}>`;
168 }
169 extractKeyOrRequiresFieldSet(directive) {
170 const arg = directive.arguments.find(arg => arg.name.value === 'fields');
171 const { value } = arg.value;
172 return (0, index_js_1.oldVisit)((0, graphql_1.parse)(`{${value}}`), {
173 leave: {
174 SelectionSet(node) {
175 return node.selections.reduce((accum, field) => {
176 accum[field.name] = field.selection;
177 return accum;
178 }, {});
179 },
180 Field(node) {
181 return {
182 name: node.name.value,
183 selection: node.selectionSet ? node.selectionSet : true,
184 };
185 },
186 Document(node) {
187 return node.definitions.find((def) => def.kind === 'OperationDefinition' && def.operation === 'query').selectionSet;
188 },
189 },
190 });
191 }
192 extractProvidesFieldSet(directive) {
193 const arg = directive.arguments.find(arg => arg.name.value === 'fields');
194 const { value } = arg.value;
195 if (/[{}]/gi.test(value)) {
196 throw new Error('Nested fields in _FieldSet is not supported in the @provides directive');
197 }
198 return value.split(/\s+/g);
199 }
200 createMapOfProvides() {
201 const providesMap = {};
202 Object.keys(this.schema.getTypeMap()).forEach(typename => {
203 const objectType = this.schema.getType(typename);
204 if ((0, graphql_1.isObjectType)(objectType)) {
205 Object.values(objectType.getFields()).forEach(field => {
206 const provides = getDirectivesByName('provides', field.astNode)
207 .map(this.extractProvidesFieldSet)
208 .reduce((prev, curr) => [...prev, ...curr], []);
209 const ofType = (0, utils_js_1.getBaseType)(field.type);
210 if (!providesMap[ofType.name]) {
211 providesMap[ofType.name] = [];
212 }
213 providesMap[ofType.name].push(...provides);
214 });
215 }
216 });
217 return providesMap;
218 }
219}
220exports.ApolloFederation = ApolloFederation;
221/**
222 * Checks if Object Type is involved in Federation. Based on `@key` directive
223 * @param node Type
224 */
225function isFederationObjectType(node, schema) {
226 const { name: { value: name }, directives, } = (0, graphql_1.isObjectType)(node) ? (0, utils_1.astFromObjectType)(node, schema) : node;
227 const rootTypeNames = (0, utils_1.getRootTypeNames)(schema);
228 const isNotRoot = !rootTypeNames.has(name);
229 const isNotIntrospection = !name.startsWith('__');
230 const hasKeyDirective = directives.some(d => d.name.value === 'key');
231 return isNotRoot && isNotIntrospection && hasKeyDirective;
232}
233/**
234 * Extracts directives from a node based on directive's name
235 * @param name directive name
236 * @param node ObjectType or Field
237 */
238function getDirectivesByName(name, node) {
239 var _a;
240 let astNode;
241 if ((0, graphql_1.isObjectType)(node)) {
242 astNode = node.astNode;
243 }
244 else {
245 astNode = node;
246 }
247 return ((_a = astNode === null || astNode === void 0 ? void 0 : astNode.directives) === null || _a === void 0 ? void 0 : _a.filter(d => d.name.value === name)) || [];
248}
249/**
250 * Checks if the Object Type extends a federated type from a remote schema.
251 * Based on if any of its fields contain the `@external` directive
252 * @param node Type
253 */
254function isTypeExtension(node, schema) {
255 var _a;
256 const definition = (0, graphql_1.isObjectType)(node) ? node.astNode || (0, utils_1.astFromObjectType)(node, schema) : node;
257 return (_a = definition.fields) === null || _a === void 0 ? void 0 : _a.some(field => getDirectivesByName('external', field).length);
258}