1 | import { parse, GraphQLObjectType, isObjectType, } from 'graphql';
|
2 | import merge from 'lodash/merge.js';
|
3 | import { getBaseType } from './utils.js';
|
4 | import { MapperKind, mapSchema, astFromObjectType, getRootTypeNames } from '@graphql-tools/utils';
|
5 | import { oldVisit } from './index.js';
|
6 |
|
7 |
|
8 |
|
9 | export const federationSpec = parse( `
|
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 |
|
19 |
|
20 |
|
21 | export 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 |
|
40 |
|
41 |
|
42 |
|
43 | export 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 | }
|
66 | const resolveReferenceFieldName = '__resolveReference';
|
67 | export 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 |
|
76 |
|
77 |
|
78 | filterTypeNames(typeNames) {
|
79 | return this.enabled ? typeNames.filter(t => t !== '_FieldSet') : typeNames;
|
80 | }
|
81 | |
82 |
|
83 |
|
84 |
|
85 | filterFieldNames(fieldNames) {
|
86 | return this.enabled ? fieldNames.filter(t => t !== resolveReferenceFieldName) : fieldNames;
|
87 | }
|
88 | |
89 |
|
90 |
|
91 |
|
92 | skipDirective(name) {
|
93 | return this.enabled && ['external', 'requires', 'provides', 'key'].includes(name);
|
94 | }
|
95 | |
96 |
|
97 |
|
98 |
|
99 | skipScalar(name) {
|
100 | return this.enabled && name === '_FieldSet';
|
101 | }
|
102 | |
103 |
|
104 |
|
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 |
|
118 |
|
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 |
|
129 | const requires = getDirectivesByName('requires', fieldNode).map(this.extractKeyOrRequiresFieldSet);
|
130 | const requiredFields = this.translateFieldSet(merge({}, ...requires), parentTypeSignature);
|
131 |
|
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 |
|
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 |
|
216 |
|
217 |
|
218 | function 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 |
|
228 |
|
229 |
|
230 |
|
231 | function 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 |
|
244 |
|
245 |
|
246 |
|
247 | function 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 | }
|