UNPKG

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