1 | import { paramCase } from 'param-case';
|
2 | import { isListType, isNonNullType, visit, Kind, isObjectType, parse, GraphQLObjectType, printType } from 'graphql';
|
3 | import { merge } from 'lodash';
|
4 |
|
5 | function resolveExternalModuleAndFn(pointer) {
|
6 |
|
7 | const importExternally = (moduleName) => eval(`require('${moduleName}')`);
|
8 | if (typeof pointer === 'function') {
|
9 | return pointer;
|
10 | }
|
11 |
|
12 | let [moduleName, functionName] = pointer.split('#');
|
13 |
|
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 |
|
29 | function isComplexPluginOutput(obj) {
|
30 | return typeof obj === 'object' && obj.hasOwnProperty('content');
|
31 | }
|
32 |
|
33 | function 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 | }
|
49 | function isWrapperType(t) {
|
50 | return isListType(t) || isNonNullType(t);
|
51 | }
|
52 | function getBaseType(type) {
|
53 | if (isWrapperType(type)) {
|
54 | return getBaseType(type.ofType);
|
55 | }
|
56 | else {
|
57 | return type;
|
58 | }
|
59 | }
|
60 |
|
61 | function isOutputConfigArray(type) {
|
62 | return Array.isArray(type);
|
63 | }
|
64 | function isConfiguredOutput(type) {
|
65 | return typeof type === 'object' && type.plugins;
|
66 | }
|
67 | function normalizeOutputParam(config) {
|
68 |
|
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 | }
|
83 | function normalizeInstanceOrArray(type) {
|
84 | if (Array.isArray(type)) {
|
85 | return type;
|
86 | }
|
87 | else if (!type) {
|
88 | return [];
|
89 | }
|
90 | return [type];
|
91 | }
|
92 | function 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 | }
|
106 | function 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 | }
|
115 | function 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 |
|
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 |
|
222 |
|
223 | const federationSpec = parse( `
|
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 |
|
233 |
|
234 |
|
235 | function 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 |
|
270 |
|
271 |
|
272 |
|
273 | function 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 | }
|
284 | const resolveReferenceFieldName = '__resolveReference';
|
285 | class 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 |
|
294 |
|
295 |
|
296 | filterTypeNames(typeNames) {
|
297 | return this.enabled ? typeNames.filter(t => t !== '_FieldSet') : typeNames;
|
298 | }
|
299 | |
300 |
|
301 |
|
302 |
|
303 | filterFieldNames(fieldNames) {
|
304 | return this.enabled ? fieldNames.filter(t => t !== resolveReferenceFieldName) : fieldNames;
|
305 | }
|
306 | |
307 |
|
308 |
|
309 |
|
310 | skipDirective(name) {
|
311 | return this.enabled && ['external', 'requires', 'provides', 'key'].includes(name);
|
312 | }
|
313 | |
314 |
|
315 |
|
316 |
|
317 | skipScalar(name) {
|
318 | return this.enabled && name === '_FieldSet';
|
319 | }
|
320 | |
321 |
|
322 |
|
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 |
|
336 |
|
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 |
|
347 | const requires = getDirectivesByName('requires', fieldNode).map(this.extractKeyOrRequiresFieldSet);
|
348 | const requiredFields = this.translateFieldSet(merge({}, ...requires), parentTypeSignature);
|
349 |
|
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 |
|
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 |
|
434 |
|
435 |
|
436 | function 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 |
|
449 |
|
450 |
|
451 |
|
452 | function 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 |
|
467 |
|
468 |
|
469 |
|
470 | function 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 |
|
478 | class 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 | }
|
488 | function isDetailedError(error) {
|
489 | return error.details;
|
490 | }
|
491 |
|
492 | export { ApolloFederation, DetailedError, addFederationReferencesToSchema, federationSpec, getBaseType, hasNullableTypeRecursively, isComplexPluginOutput, isConfiguredOutput, isDetailedError, isOutputConfigArray, isUsingTypes, isWrapperType, mergeOutputs, normalizeConfig, normalizeInstanceOrArray, normalizeOutputParam, removeFederation, resolveExternalModuleAndFn };
|
493 |
|