1 | import { paramCase } from 'param-case';
|
2 | import { isListType, isNonNullType, visit, Kind, isObjectType, parse, GraphQLObjectType, printType } from 'graphql';
|
3 |
|
4 | function resolveExternalModuleAndFn(pointer) {
|
5 |
|
6 | const importExternally = (moduleName) => eval(`require('${moduleName}')`);
|
7 | if (typeof pointer === 'function') {
|
8 | return pointer;
|
9 | }
|
10 |
|
11 | let [moduleName, functionName] = pointer.split('#');
|
12 |
|
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 |
|
28 | function isComplexPluginOutput(obj) {
|
29 | return typeof obj === 'object' && obj.hasOwnProperty('content');
|
30 | }
|
31 |
|
32 | function 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 | }
|
48 | function isWrapperType(t) {
|
49 | return isListType(t) || isNonNullType(t);
|
50 | }
|
51 | function getBaseType(type) {
|
52 | if (isWrapperType(type)) {
|
53 | return getBaseType(type.ofType);
|
54 | }
|
55 | else {
|
56 | return type;
|
57 | }
|
58 | }
|
59 |
|
60 | function isOutputConfigArray(type) {
|
61 | return Array.isArray(type);
|
62 | }
|
63 | function isConfiguredOutput(type) {
|
64 | return typeof type === 'object' && type.plugins;
|
65 | }
|
66 | function normalizeOutputParam(config) {
|
67 |
|
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 | }
|
82 | function normalizeInstanceOrArray(type) {
|
83 | if (Array.isArray(type)) {
|
84 | return type;
|
85 | }
|
86 | else if (!type) {
|
87 | return [];
|
88 | }
|
89 | return [type];
|
90 | }
|
91 | function 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 | }
|
105 | function 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 | }
|
114 | function 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 |
|
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 |
|
221 |
|
222 | const federationSpec = parse( `
|
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 |
|
232 |
|
233 |
|
234 | function 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 |
|
269 |
|
270 |
|
271 |
|
272 | function 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 | }
|
283 | const resolveReferenceFieldName = '__resolveReference';
|
284 | class 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 |
|
293 |
|
294 |
|
295 | filterTypeNames(typeNames) {
|
296 | return this.enabled ? typeNames.filter(t => t !== '_FieldSet') : typeNames;
|
297 | }
|
298 | |
299 |
|
300 |
|
301 |
|
302 | filterFieldNames(fieldNames) {
|
303 | return this.enabled ? fieldNames.filter(t => t !== resolveReferenceFieldName) : fieldNames;
|
304 | }
|
305 | |
306 |
|
307 |
|
308 |
|
309 | skipDirective(name) {
|
310 | return this.enabled && ['external', 'requires', 'provides', 'key'].includes(name);
|
311 | }
|
312 | |
313 |
|
314 |
|
315 |
|
316 | skipScalar(name) {
|
317 | return this.enabled && name === '_FieldSet';
|
318 | }
|
319 | |
320 |
|
321 |
|
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 |
|
335 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
417 |
|
418 |
|
419 | function 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 | }
|
430 | function deduplicate(items) {
|
431 | return items.filter((item, i) => items.indexOf(item) === i);
|
432 | }
|
433 |
|
434 |
|
435 |
|
436 |
|
437 |
|
438 | function 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 |
|
452 | class 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 | }
|
462 | function isDetailedError(error) {
|
463 | return error.details;
|
464 | }
|
465 |
|
466 | export { ApolloFederation, DetailedError, addFederationReferencesToSchema, federationSpec, getBaseType, hasNullableTypeRecursively, isComplexPluginOutput, isConfiguredOutput, isDetailedError, isOutputConfigArray, isUsingTypes, isWrapperType, mergeOutputs, normalizeConfig, normalizeInstanceOrArray, normalizeOutputParam, removeFederation, resolveExternalModuleAndFn };
|
467 |
|