UNPKG

19 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5const paramCase = require('param-case');
6const graphql = require('graphql');
7
8function resolveExternalModuleAndFn(pointer) {
9 // eslint-disable-next-line no-eval
10 const importExternally = (moduleName) => eval(`require('${moduleName}')`);
11 if (typeof pointer === 'function') {
12 return pointer;
13 }
14 // eslint-disable-next-line prefer-const
15 let [moduleName, functionName] = pointer.split('#');
16 // Temp workaround until v2
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
32function isComplexPluginOutput(obj) {
33 return typeof obj === 'object' && obj.hasOwnProperty('content');
34}
35
36function 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}
52function isWrapperType(t) {
53 return graphql.isListType(t) || graphql.isNonNullType(t);
54}
55function getBaseType(type) {
56 if (isWrapperType(type)) {
57 return getBaseType(type.ofType);
58 }
59 else {
60 return type;
61 }
62}
63
64function isOutputConfigArray(type) {
65 return Array.isArray(type);
66}
67function isConfiguredOutput(type) {
68 return typeof type === 'object' && type.plugins;
69}
70function normalizeOutputParam(config) {
71 // In case of direct array with a list of plugins
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}
86function normalizeInstanceOrArray(type) {
87 if (Array.isArray(type)) {
88 return type;
89 }
90 else if (!type) {
91 return [];
92 }
93 return [type];
94}
95function 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}
109function 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}
118function 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 // To handle `Maybe` usage
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 * Federation Spec
225 */
226const federationSpec = graphql.parse(/* GraphQL */ `
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 * Adds `__resolveReference` in each ObjectType involved in Federation.
236 * @param schema
237 */
238function 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 * Removes Federation Spec from GraphQL Schema
273 * @param schema
274 * @param config
275 */
276function 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}
287const resolveReferenceFieldName = '__resolveReference';
288class 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 * Excludes types definde by Federation
297 * @param typeNames List of type names
298 */
299 filterTypeNames(typeNames) {
300 return this.enabled ? typeNames.filter(t => t !== '_FieldSet') : typeNames;
301 }
302 /**
303 * Excludes `__resolveReference` fields
304 * @param fieldNames List of field names
305 */
306 filterFieldNames(fieldNames) {
307 return this.enabled ? fieldNames.filter(t => t !== resolveReferenceFieldName) : fieldNames;
308 }
309 /**
310 * Decides if directive should not be generated
311 * @param name directive's name
312 */
313 skipDirective(name) {
314 return this.enabled && ['external', 'requires', 'provides', 'key'].includes(name);
315 }
316 /**
317 * Decides if scalar should not be generated
318 * @param name directive's name
319 */
320 skipScalar(name) {
321 return this.enabled && name === '_FieldSet';
322 }
323 /**
324 * Decides if field should not be generated
325 * @param data
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 * Transforms ParentType signature in ObjectTypes involved in Federation
339 * @param data
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 // Look for @requires and see what the service needs and gets
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 // @key() @key() - "primary keys" in Federation
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 // include required fields
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 // TODO: support other things than fields separated by a whitespace (fields: "fieldA fieldB fieldC")
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 * Checks if Object Type is involved in Federation. Based on `@key` directive
421 * @param node Type
422 */
423function 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}
434function deduplicate(items) {
435 return items.filter((item, i) => items.indexOf(item) === i);
436}
437/**
438 * Extracts directives from a node based on directive's name
439 * @param name directive name
440 * @param node ObjectType or Field
441 */
442function 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
456class 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}
466function isDetailedError(error) {
467 return error.details;
468}
469
470exports.ApolloFederation = ApolloFederation;
471exports.DetailedError = DetailedError;
472exports.addFederationReferencesToSchema = addFederationReferencesToSchema;
473exports.federationSpec = federationSpec;
474exports.getBaseType = getBaseType;
475exports.hasNullableTypeRecursively = hasNullableTypeRecursively;
476exports.isComplexPluginOutput = isComplexPluginOutput;
477exports.isConfiguredOutput = isConfiguredOutput;
478exports.isDetailedError = isDetailedError;
479exports.isOutputConfigArray = isOutputConfigArray;
480exports.isUsingTypes = isUsingTypes;
481exports.isWrapperType = isWrapperType;
482exports.mergeOutputs = mergeOutputs;
483exports.normalizeConfig = normalizeConfig;
484exports.normalizeInstanceOrArray = normalizeInstanceOrArray;
485exports.normalizeOutputParam = normalizeOutputParam;
486exports.removeFederation = removeFederation;
487exports.resolveExternalModuleAndFn = resolveExternalModuleAndFn;
488//# sourceMappingURL=index.cjs.js.map