UNPKG

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