1 | 'use strict';
|
2 |
|
3 | Object.defineProperty(exports, '__esModule', { value: true });
|
4 |
|
5 | const paramCase = require('param-case');
|
6 | const graphql = require('graphql');
|
7 |
|
8 | function resolveExternalModuleAndFn(pointer) {
|
9 |
|
10 | const importExternally = (moduleName) => eval(`require('${moduleName}')`);
|
11 | if (typeof pointer === 'function') {
|
12 | return pointer;
|
13 | }
|
14 |
|
15 | let [moduleName, functionName] = pointer.split('#');
|
16 |
|
17 | if (moduleName === 'change-case') {
|
18 | moduleName = paramCase.paramCase(functionName);
|
19 | }
|
20 | const { resolve } = importExternally('path');
|
21 | const localFilePath = resolve(process.cwd(), moduleName);
|
22 | const { existsSync } = importExternally('fs');
|
23 | const localFileExists = existsSync(localFilePath);
|
24 | const importFrom = importExternally('import-from');
|
25 | const loadedModule = localFileExists ? importExternally(localFilePath) : importFrom(process.cwd(), moduleName);
|
26 | if (!(functionName in loadedModule) && typeof loadedModule !== 'function') {
|
27 | throw new Error(`${functionName} couldn't be found in module ${moduleName}!`);
|
28 | }
|
29 | return loadedModule[functionName] || loadedModule;
|
30 | }
|
31 |
|
32 | function isComplexPluginOutput(obj) {
|
33 | return typeof obj === 'object' && obj.hasOwnProperty('content');
|
34 | }
|
35 |
|
36 | function mergeOutputs(content) {
|
37 | const result = { content: '', prepend: [], append: [] };
|
38 | if (Array.isArray(content)) {
|
39 | content.forEach(item => {
|
40 | if (typeof item === 'string') {
|
41 | result.content += item;
|
42 | }
|
43 | else {
|
44 | result.content += item.content;
|
45 | result.prepend.push(...(item.prepend || []));
|
46 | result.append.push(...(item.append || []));
|
47 | }
|
48 | });
|
49 | }
|
50 | return [...result.prepend, result.content, ...result.append].join('\n');
|
51 | }
|
52 | function isWrapperType(t) {
|
53 | return graphql.isListType(t) || graphql.isNonNullType(t);
|
54 | }
|
55 | function getBaseType(type) {
|
56 | if (isWrapperType(type)) {
|
57 | return getBaseType(type.ofType);
|
58 | }
|
59 | else {
|
60 | return type;
|
61 | }
|
62 | }
|
63 |
|
64 | function isOutputConfigArray(type) {
|
65 | return Array.isArray(type);
|
66 | }
|
67 | function isConfiguredOutput(type) {
|
68 | return typeof type === 'object' && type.plugins;
|
69 | }
|
70 | function normalizeOutputParam(config) {
|
71 |
|
72 | if (isOutputConfigArray(config)) {
|
73 | return {
|
74 | documents: [],
|
75 | schema: [],
|
76 | plugins: isConfiguredOutput(config) ? config.plugins : config,
|
77 | };
|
78 | }
|
79 | else if (isConfiguredOutput(config)) {
|
80 | return config;
|
81 | }
|
82 | else {
|
83 | throw new Error(`Invalid "generates" config!`);
|
84 | }
|
85 | }
|
86 | function normalizeInstanceOrArray(type) {
|
87 | if (Array.isArray(type)) {
|
88 | return type;
|
89 | }
|
90 | else if (!type) {
|
91 | return [];
|
92 | }
|
93 | return [type];
|
94 | }
|
95 | function normalizeConfig(config) {
|
96 | if (typeof config === 'string') {
|
97 | return [{ [config]: {} }];
|
98 | }
|
99 | else if (Array.isArray(config)) {
|
100 | return config.map(plugin => (typeof plugin === 'string' ? { [plugin]: {} } : plugin));
|
101 | }
|
102 | else if (typeof config === 'object') {
|
103 | return Object.keys(config).reduce((prev, pluginName) => [...prev, { [pluginName]: config[pluginName] }], []);
|
104 | }
|
105 | else {
|
106 | return [];
|
107 | }
|
108 | }
|
109 | function hasNullableTypeRecursively(type) {
|
110 | if (!graphql.isNonNullType(type)) {
|
111 | return true;
|
112 | }
|
113 | if (graphql.isListType(type) || graphql.isNonNullType(type)) {
|
114 | return hasNullableTypeRecursively(type.ofType);
|
115 | }
|
116 | return false;
|
117 | }
|
118 | function isUsingTypes(document, externalFragments, schema) {
|
119 | let foundFields = 0;
|
120 | const typesStack = [];
|
121 | graphql.visit(document, {
|
122 | SelectionSet: {
|
123 | enter(node, key, parent, anscestors) {
|
124 | const insideIgnoredFragment = anscestors.find((f) => f.kind && f.kind === 'FragmentDefinition' && externalFragments.includes(f.name.value));
|
125 | if (insideIgnoredFragment) {
|
126 | return;
|
127 | }
|
128 | const selections = node.selections || [];
|
129 | if (schema && selections.length > 0) {
|
130 | const nextTypeName = (() => {
|
131 | if (parent.kind === graphql.Kind.FRAGMENT_DEFINITION) {
|
132 | return parent.typeCondition.name.value;
|
133 | }
|
134 | else if (parent.kind === graphql.Kind.FIELD) {
|
135 | const lastType = typesStack[typesStack.length - 1];
|
136 | if (!lastType) {
|
137 | throw new Error(`Unable to find parent type! Please make sure you operation passes validation`);
|
138 | }
|
139 | const field = lastType.getFields()[parent.name.value];
|
140 | if (!field) {
|
141 | throw new Error(`Unable to find field "${parent.name.value}" on type "${lastType}"!`);
|
142 | }
|
143 | return getBaseType(field.type).name;
|
144 | }
|
145 | else if (parent.kind === graphql.Kind.OPERATION_DEFINITION) {
|
146 | if (parent.operation === 'query') {
|
147 | return schema.getQueryType().name;
|
148 | }
|
149 | else if (parent.operation === 'mutation') {
|
150 | return schema.getMutationType().name;
|
151 | }
|
152 | else if (parent.operation === 'subscription') {
|
153 | return schema.getSubscriptionType().name;
|
154 | }
|
155 | }
|
156 | else if (parent.kind === graphql.Kind.INLINE_FRAGMENT && parent.typeCondition) {
|
157 | return parent.typeCondition.name.value;
|
158 | }
|
159 | return null;
|
160 | })();
|
161 | typesStack.push(schema.getType(nextTypeName));
|
162 | }
|
163 | },
|
164 | leave(node) {
|
165 | const selections = node.selections || [];
|
166 | if (schema && selections.length > 0) {
|
167 | typesStack.pop();
|
168 | }
|
169 | },
|
170 | },
|
171 | Field: {
|
172 | enter: (node, key, parent, path, anscestors) => {
|
173 | if (node.name.value.startsWith('__')) {
|
174 | return;
|
175 | }
|
176 | const insideIgnoredFragment = anscestors.find((f) => f.kind && f.kind === 'FragmentDefinition' && externalFragments.includes(f.name.value));
|
177 | if (insideIgnoredFragment) {
|
178 | return;
|
179 | }
|
180 | const selections = node.selectionSet ? node.selectionSet.selections || [] : [];
|
181 | const relevantFragmentSpreads = selections.filter(s => s.kind === graphql.Kind.FRAGMENT_SPREAD && !externalFragments.includes(s.name.value));
|
182 | if (selections.length === 0 || relevantFragmentSpreads.length > 0) {
|
183 | foundFields++;
|
184 | }
|
185 | if (schema) {
|
186 | const lastType = typesStack[typesStack.length - 1];
|
187 | if (lastType) {
|
188 | if (graphql.isObjectType(lastType)) {
|
189 | const field = lastType.getFields()[node.name.value];
|
190 | if (!field) {
|
191 | throw new Error(`Unable to find field "${node.name.value}" on type "${lastType}"!`);
|
192 | }
|
193 | const currentType = field.type;
|
194 |
|
195 | if (hasNullableTypeRecursively(currentType)) {
|
196 | foundFields++;
|
197 | }
|
198 | }
|
199 | }
|
200 | }
|
201 | },
|
202 | },
|
203 | enter: {
|
204 | VariableDefinition: (node, key, parent, path, anscestors) => {
|
205 | const insideIgnoredFragment = anscestors.find((f) => f.kind && f.kind === 'FragmentDefinition' && externalFragments.includes(f.name.value));
|
206 | if (insideIgnoredFragment) {
|
207 | return;
|
208 | }
|
209 | foundFields++;
|
210 | },
|
211 | InputValueDefinition: (node, key, parent, path, anscestors) => {
|
212 | const insideIgnoredFragment = anscestors.find((f) => f.kind && f.kind === 'FragmentDefinition' && externalFragments.includes(f.name.value));
|
213 | if (insideIgnoredFragment) {
|
214 | return;
|
215 | }
|
216 | foundFields++;
|
217 | },
|
218 | },
|
219 | });
|
220 | return foundFields > 0;
|
221 | }
|
222 |
|
223 |
|
224 |
|
225 |
|
226 | const federationSpec = graphql.parse( `
|
227 | scalar _FieldSet
|
228 |
|
229 | directive @external on FIELD_DEFINITION
|
230 | directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
|
231 | directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
|
232 | directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
|
233 | `);
|
234 |
|
235 |
|
236 |
|
237 |
|
238 | function addFederationReferencesToSchema(schema) {
|
239 | const typeMap = schema.getTypeMap();
|
240 | for (const typeName in typeMap) {
|
241 | const type = schema.getType(typeName);
|
242 | if (graphql.isObjectType(type) && isFederationObjectType(type)) {
|
243 | const typeConfig = type.toConfig();
|
244 | typeConfig.fields = {
|
245 | [resolveReferenceFieldName]: {
|
246 | type,
|
247 | },
|
248 | ...typeConfig.fields,
|
249 | };
|
250 | const newType = new graphql.GraphQLObjectType(typeConfig);
|
251 | newType.astNode = newType.astNode || graphql.parse(graphql.printType(newType)).definitions[0];
|
252 | newType.astNode.fields.unshift({
|
253 | kind: graphql.Kind.FIELD_DEFINITION,
|
254 | name: {
|
255 | kind: graphql.Kind.NAME,
|
256 | value: resolveReferenceFieldName,
|
257 | },
|
258 | type: {
|
259 | kind: graphql.Kind.NAMED_TYPE,
|
260 | name: {
|
261 | kind: graphql.Kind.NAME,
|
262 | value: typeName,
|
263 | },
|
264 | },
|
265 | });
|
266 | typeMap[typeName] = newType;
|
267 | }
|
268 | }
|
269 | return schema;
|
270 | }
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 | function removeFederation(schema) {
|
277 | const queryType = schema.getQueryType();
|
278 | const queryTypeFields = queryType.getFields();
|
279 | delete queryTypeFields._entities;
|
280 | delete queryTypeFields._service;
|
281 | const typeMap = schema.getTypeMap();
|
282 | delete typeMap._Service;
|
283 | delete typeMap._Entity;
|
284 | delete typeMap._Any;
|
285 | return schema;
|
286 | }
|
287 | const resolveReferenceFieldName = '__resolveReference';
|
288 | class ApolloFederation {
|
289 | constructor({ enabled, schema }) {
|
290 | this.enabled = false;
|
291 | this.enabled = enabled;
|
292 | this.schema = schema;
|
293 | this.providesMap = this.createMapOfProvides();
|
294 | }
|
295 | |
296 |
|
297 |
|
298 |
|
299 | filterTypeNames(typeNames) {
|
300 | return this.enabled ? typeNames.filter(t => t !== '_FieldSet') : typeNames;
|
301 | }
|
302 | |
303 |
|
304 |
|
305 |
|
306 | filterFieldNames(fieldNames) {
|
307 | return this.enabled ? fieldNames.filter(t => t !== resolveReferenceFieldName) : fieldNames;
|
308 | }
|
309 | |
310 |
|
311 |
|
312 |
|
313 | skipDirective(name) {
|
314 | return this.enabled && ['external', 'requires', 'provides', 'key'].includes(name);
|
315 | }
|
316 | |
317 |
|
318 |
|
319 |
|
320 | skipScalar(name) {
|
321 | return this.enabled && name === '_FieldSet';
|
322 | }
|
323 | |
324 |
|
325 |
|
326 |
|
327 | skipField({ fieldNode, parentType }) {
|
328 | if (!this.enabled || !graphql.isObjectType(parentType) || !isFederationObjectType(parentType)) {
|
329 | return false;
|
330 | }
|
331 | return this.isExternalAndNotProvided(fieldNode, parentType);
|
332 | }
|
333 | isResolveReferenceField(fieldNode) {
|
334 | const name = typeof fieldNode.name === 'string' ? fieldNode.name : fieldNode.name.value;
|
335 | return this.enabled && name === resolveReferenceFieldName;
|
336 | }
|
337 | |
338 |
|
339 |
|
340 |
|
341 | transformParentType({ fieldNode, parentType, parentTypeSignature, }) {
|
342 | if (this.enabled &&
|
343 | graphql.isObjectType(parentType) &&
|
344 | isFederationObjectType(parentType) &&
|
345 | fieldNode.name.value === resolveReferenceFieldName) {
|
346 | const keys = getDirectivesByName('key', parentType);
|
347 | if (keys.length) {
|
348 | const outputs = [`{ __typename: '${parentType.name}' } &`];
|
349 |
|
350 | const requires = getDirectivesByName('requires', fieldNode)
|
351 | .map(this.extractFieldSet)
|
352 | .reduce((prev, curr) => [...prev, ...curr], [])
|
353 | .map(name => {
|
354 | return { name, required: graphql.isNonNullType(parentType.getFields()[name].type) };
|
355 | });
|
356 | const requiredFields = this.translateFieldSet(requires, parentTypeSignature);
|
357 |
|
358 | const primaryKeys = keys.map(def => {
|
359 | const fields = this.extractFieldSet(def).map(name => ({ name, required: true }));
|
360 | return this.translateFieldSet(fields, parentTypeSignature);
|
361 | });
|
362 | const [open, close] = primaryKeys.length > 1 ? ['(', ')'] : ['', ''];
|
363 | outputs.push([open, primaryKeys.join(' | '), close].join(''));
|
364 |
|
365 | if (requires.length) {
|
366 | outputs.push(`& ${requiredFields}`);
|
367 | }
|
368 | return outputs.join(' ');
|
369 | }
|
370 | }
|
371 | return parentTypeSignature;
|
372 | }
|
373 | isExternalAndNotProvided(fieldNode, objectType) {
|
374 | return this.isExternal(fieldNode) && !this.hasProvides(objectType, fieldNode);
|
375 | }
|
376 | isExternal(node) {
|
377 | return getDirectivesByName('external', node).length > 0;
|
378 | }
|
379 | hasProvides(objectType, node) {
|
380 | const fields = this.providesMap[graphql.isObjectType(objectType) ? objectType.name : objectType.name.value];
|
381 | if (fields && fields.length) {
|
382 | return fields.includes(node.name.value);
|
383 | }
|
384 | return false;
|
385 | }
|
386 | translateFieldSet(fields, parentTypeRef) {
|
387 |
|
388 | const keys = fields.map(field => `'${field.name}'`).join(' | ');
|
389 | return `Pick<${parentTypeRef}, ${keys}>`;
|
390 | }
|
391 | extractFieldSet(directive) {
|
392 | const arg = directive.arguments.find(arg => arg.name.value === 'fields');
|
393 | const value = arg.value.value;
|
394 | if (/[{}]/gi.test(value)) {
|
395 | throw new Error('Nested fields in _FieldSet is not supported');
|
396 | }
|
397 | return deduplicate(value.split(/\s+/g));
|
398 | }
|
399 | createMapOfProvides() {
|
400 | const providesMap = {};
|
401 | Object.keys(this.schema.getTypeMap()).forEach(typename => {
|
402 | const objectType = this.schema.getType(typename);
|
403 | if (graphql.isObjectType(objectType)) {
|
404 | Object.values(objectType.getFields()).forEach(field => {
|
405 | const provides = getDirectivesByName('provides', field.astNode)
|
406 | .map(this.extractFieldSet)
|
407 | .reduce((prev, curr) => [...prev, ...curr], []);
|
408 | const ofType = getBaseType(field.type);
|
409 | if (!providesMap[ofType.name]) {
|
410 | providesMap[ofType.name] = [];
|
411 | }
|
412 | providesMap[ofType.name].push(...provides);
|
413 | });
|
414 | }
|
415 | });
|
416 | return providesMap;
|
417 | }
|
418 | }
|
419 |
|
420 |
|
421 |
|
422 |
|
423 | function isFederationObjectType(node) {
|
424 | const definition = graphql.isObjectType(node)
|
425 | ? node.astNode || graphql.parse(graphql.printType(node)).definitions[0]
|
426 | : node;
|
427 | const name = definition.name.value;
|
428 | const directives = definition.directives;
|
429 | const isNotRoot = !['Query', 'Mutation', 'Subscription'].includes(name);
|
430 | const isNotIntrospection = !name.startsWith('__');
|
431 | const hasKeyDirective = directives.some(d => d.name.value === 'key');
|
432 | return isNotRoot && isNotIntrospection && hasKeyDirective;
|
433 | }
|
434 | function deduplicate(items) {
|
435 | return items.filter((item, i) => items.indexOf(item) === i);
|
436 | }
|
437 |
|
438 |
|
439 |
|
440 |
|
441 |
|
442 | function getDirectivesByName(name, node) {
|
443 | let astNode;
|
444 | if (graphql.isObjectType(node)) {
|
445 | astNode = node.astNode;
|
446 | }
|
447 | else {
|
448 | astNode = node;
|
449 | }
|
450 | if (astNode && astNode.directives) {
|
451 | return astNode.directives.filter(d => d.name.value === name);
|
452 | }
|
453 | return [];
|
454 | }
|
455 |
|
456 | class DetailedError extends Error {
|
457 | constructor(message, details, source) {
|
458 | super(message);
|
459 | this.message = message;
|
460 | this.details = details;
|
461 | this.source = source;
|
462 | Object.setPrototypeOf(this, DetailedError.prototype);
|
463 | Error.captureStackTrace(this, DetailedError);
|
464 | }
|
465 | }
|
466 | function isDetailedError(error) {
|
467 | return error.details;
|
468 | }
|
469 |
|
470 | exports.ApolloFederation = ApolloFederation;
|
471 | exports.DetailedError = DetailedError;
|
472 | exports.addFederationReferencesToSchema = addFederationReferencesToSchema;
|
473 | exports.federationSpec = federationSpec;
|
474 | exports.getBaseType = getBaseType;
|
475 | exports.hasNullableTypeRecursively = hasNullableTypeRecursively;
|
476 | exports.isComplexPluginOutput = isComplexPluginOutput;
|
477 | exports.isConfiguredOutput = isConfiguredOutput;
|
478 | exports.isDetailedError = isDetailedError;
|
479 | exports.isOutputConfigArray = isOutputConfigArray;
|
480 | exports.isUsingTypes = isUsingTypes;
|
481 | exports.isWrapperType = isWrapperType;
|
482 | exports.mergeOutputs = mergeOutputs;
|
483 | exports.normalizeConfig = normalizeConfig;
|
484 | exports.normalizeInstanceOrArray = normalizeInstanceOrArray;
|
485 | exports.normalizeOutputParam = normalizeOutputParam;
|
486 | exports.removeFederation = removeFederation;
|
487 | exports.resolveExternalModuleAndFn = resolveExternalModuleAndFn;
|
488 |
|