UNPKG

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