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