UNPKG

20 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5const paramCase = require('param-case');
6const graphql = require('graphql');
7const lodash = require('lodash');
8
9function resolveExternalModuleAndFn(pointer) {
10 // eslint-disable-next-line no-eval
11 const importExternally = (moduleName) => eval(`require('${moduleName}')`);
12 if (typeof pointer === 'function') {
13 return pointer;
14 }
15 // eslint-disable-next-line prefer-const
16 let [moduleName, functionName] = pointer.split('#');
17 // Temp workaround until v2
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
33function isComplexPluginOutput(obj) {
34 return typeof obj === 'object' && obj.hasOwnProperty('content');
35}
36
37function 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}
53function isWrapperType(t) {
54 return graphql.isListType(t) || graphql.isNonNullType(t);
55}
56function getBaseType(type) {
57 if (isWrapperType(type)) {
58 return getBaseType(type.ofType);
59 }
60 else {
61 return type;
62 }
63}
64
65function isOutputConfigArray(type) {
66 return Array.isArray(type);
67}
68function isConfiguredOutput(type) {
69 return typeof type === 'object' && type.plugins;
70}
71function normalizeOutputParam(config) {
72 // In case of direct array with a list of plugins
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}
87function normalizeInstanceOrArray(type) {
88 if (Array.isArray(type)) {
89 return type;
90 }
91 else if (!type) {
92 return [];
93 }
94 return [type];
95}
96function 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}
110function 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}
119function 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 // To handle `Maybe` usage
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 * Federation Spec
226 */
227const federationSpec = graphql.parse(/* GraphQL */ `
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 * Adds `__resolveReference` in each ObjectType involved in Federation.
237 * @param schema
238 */
239function 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 * Removes Federation Spec from GraphQL Schema
274 * @param schema
275 * @param config
276 */
277function 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}
288const resolveReferenceFieldName = '__resolveReference';
289class 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 * Excludes types definde by Federation
298 * @param typeNames List of type names
299 */
300 filterTypeNames(typeNames) {
301 return this.enabled ? typeNames.filter(t => t !== '_FieldSet') : typeNames;
302 }
303 /**
304 * Excludes `__resolveReference` fields
305 * @param fieldNames List of field names
306 */
307 filterFieldNames(fieldNames) {
308 return this.enabled ? fieldNames.filter(t => t !== resolveReferenceFieldName) : fieldNames;
309 }
310 /**
311 * Decides if directive should not be generated
312 * @param name directive's name
313 */
314 skipDirective(name) {
315 return this.enabled && ['external', 'requires', 'provides', 'key'].includes(name);
316 }
317 /**
318 * Decides if scalar should not be generated
319 * @param name directive's name
320 */
321 skipScalar(name) {
322 return this.enabled && name === '_FieldSet';
323 }
324 /**
325 * Decides if field should not be generated
326 * @param data
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 * Transforms ParentType signature in ObjectTypes involved in Federation
340 * @param data
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 // Look for @requires and see what the service needs and gets
351 const requires = getDirectivesByName('requires', fieldNode).map(this.extractKeyOrRequiresFieldSet);
352 const requiredFields = this.translateFieldSet(lodash.merge({}, ...requires), parentTypeSignature);
353 // @key() @key() - "primary keys" in Federation
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 // include required fields
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 * Checks if Object Type is involved in Federation. Based on `@key` directive
438 * @param node Type
439 */
440function 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 * Extracts directives from a node based on directive's name
453 * @param name directive name
454 * @param node ObjectType or Field
455 */
456function 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 * Checks if the Object Type extends a federated type from a remote schema.
471 * Based on if any of its fields contain the `@external` directive
472 * @param node Type
473 */
474function 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
482class 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}
492function isDetailedError(error) {
493 return error.details;
494}
495
496exports.ApolloFederation = ApolloFederation;
497exports.DetailedError = DetailedError;
498exports.addFederationReferencesToSchema = addFederationReferencesToSchema;
499exports.federationSpec = federationSpec;
500exports.getBaseType = getBaseType;
501exports.hasNullableTypeRecursively = hasNullableTypeRecursively;
502exports.isComplexPluginOutput = isComplexPluginOutput;
503exports.isConfiguredOutput = isConfiguredOutput;
504exports.isDetailedError = isDetailedError;
505exports.isOutputConfigArray = isOutputConfigArray;
506exports.isUsingTypes = isUsingTypes;
507exports.isWrapperType = isWrapperType;
508exports.mergeOutputs = mergeOutputs;
509exports.normalizeConfig = normalizeConfig;
510exports.normalizeInstanceOrArray = normalizeInstanceOrArray;
511exports.normalizeOutputParam = normalizeOutputParam;
512exports.removeFederation = removeFederation;
513exports.resolveExternalModuleAndFn = resolveExternalModuleAndFn;
514//# sourceMappingURL=index.cjs.js.map