Skip to content

Commit b8e4ab5

Browse files
Handle edge cases with subgraph extraction logic (#3136)
While reviewing recent code changes that released in `2.9.0`, I noticed a few bugs in the code. This PR contains fixes for subgraph extraction bugs. Specifically, this PR updates subgraph extraction to use pre-existing functions/patterns instead of repeating logic, to avoid copy/pasting and bugs/divergence (the copied logic wasn't looking at spec renaming, for example).
1 parent c7d0e8e commit b8e4ab5

4 files changed

Lines changed: 118 additions & 96 deletions

File tree

.changeset/light-ties-chew.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/federation-internals": patch
3+
---
4+
5+
Fix edge cases for subgraph extraction logic when using spec renaming or specs URLs that look similar to `specs.apollo.dev`.

internals-js/src/extractSubgraphsFromSupergraph.ts

Lines changed: 73 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { parseSelectionSet } from "./operations";
4040
import fs from 'fs';
4141
import path from 'path';
4242
import { validateStringContainsBoolean } from "./utils";
43-
import { CONTEXT_VERSIONS, ContextSpecDefinition, DirectiveDefinition, FeatureUrl, FederationDirectiveName, SchemaElement, errorCauses, isFederationDirectiveDefinedInSchema, printErrors } from ".";
43+
import { ContextSpecDefinition, CostSpecDefinition, SchemaElement, errorCauses, isFederationDirectiveDefinedInSchema, printErrors } from ".";
4444

4545
function filteredTypes(
4646
supergraph: Schema,
@@ -194,7 +194,7 @@ function typesUsedInFederationDirective(fieldSet: string | undefined, parentType
194194
}
195195

196196
export function extractSubgraphsFromSupergraph(supergraph: Schema, validateExtractedSubgraphs: boolean = true): [Subgraphs, Map<string, string>] {
197-
const [coreFeatures, joinSpec] = validateSupergraph(supergraph);
197+
const [coreFeatures, joinSpec, contextSpec, costSpec] = validateSupergraph(supergraph);
198198
const isFed1 = joinSpec.version.equals(new FeatureVersion(0, 1));
199199
try {
200200
// We first collect the subgraphs (creating an empty schema that we'll populate next for each).
@@ -224,13 +224,13 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema, validateExtra
224224
}
225225

226226
const types = filteredTypes(supergraph, joinSpec, coreFeatures.coreDefinition);
227-
const originalDirectiveNames = getApolloDirectiveNames(supergraph);
228227
const args: ExtractArguments = {
229228
supergraph,
230229
subgraphs,
231230
joinSpec,
231+
contextSpec,
232+
costSpec,
232233
filteredTypes: types,
233-
originalDirectiveNames,
234234
getSubgraph,
235235
getSubgraphEnumValue,
236236
};
@@ -293,8 +293,9 @@ type ExtractArguments = {
293293
supergraph: Schema,
294294
subgraphs: Subgraphs,
295295
joinSpec: JoinSpecDefinition,
296+
contextSpec: ContextSpecDefinition | undefined,
297+
costSpec: CostSpecDefinition | undefined,
296298
filteredTypes: NamedType[],
297-
originalDirectiveNames: Record<string, string>,
298299
getSubgraph: (application: Directive<any, { graph?: string }>) => Subgraph | undefined,
299300
getSubgraphEnumValue: (subgraphName: string) => string
300301
}
@@ -352,7 +353,9 @@ function addAllEmptySubgraphTypes(args: ExtractArguments): TypesInfo {
352353
const subgraph = getSubgraph(application);
353354
assert(subgraph, () => `Should have found the subgraph for ${application}`);
354355
const subgraphType = subgraph.schema.addType(newNamedType(type.kind, type.name));
355-
propagateDemandControlDirectives(type, subgraphType, subgraph, args.originalDirectiveNames);
356+
if (args.costSpec) {
357+
propagateDemandControlDirectives(type, subgraphType, subgraph, args.costSpec);
358+
}
356359
}
357360
break;
358361
}
@@ -401,17 +404,8 @@ function addEmptyType<T extends NamedType>(
401404
}
402405
}
403406
}
404-
405-
const coreFeatures = supergraph.coreFeatures;
406-
assert(coreFeatures, 'Should have core features');
407-
const contextFeature = coreFeatures.getByIdentity(ContextSpecDefinition.identity);
408-
let supergraphContextDirective: DirectiveDefinition<{ name: string}> | undefined;
409-
if (contextFeature) {
410-
const contextSpec = CONTEXT_VERSIONS.find(contextFeature.url.version);
411-
assert(contextSpec, 'Should have context spec');
412-
supergraphContextDirective = contextSpec.contextDirective(supergraph);
413-
}
414-
407+
408+
const supergraphContextDirective = args.contextSpec?.contextDirective(supergraph);
415409
if (supergraphContextDirective) {
416410
const contextApplications = type.appliedDirectivesOf(supergraphContextDirective);
417411
// for every application, apply the context directive to the correct subgraph
@@ -438,8 +432,6 @@ function extractObjOrItfContent(args: ExtractArguments, info: TypeInfo<ObjectTyp
438432
const implementsDirective = args.joinSpec.implementsDirective(args.supergraph);
439433
assert(implementsDirective, '@join__implements should existing for a fed2 supergraph');
440434

441-
const originalDirectiveNames = args.originalDirectiveNames;
442-
443435
for (const { type, subgraphsInfo } of info) {
444436
const implementsApplications = type.appliedDirectivesOf(implementsDirective);
445437
for (const application of implementsApplications) {
@@ -450,8 +442,10 @@ function extractObjOrItfContent(args: ExtractArguments, info: TypeInfo<ObjectTyp
450442
subgraphInfo.type.addImplementedInterface(args.interface);
451443
}
452444

453-
for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) {
454-
propagateDemandControlDirectives(type, subgraphType, subgraph, args.originalDirectiveNames);
445+
if (args.costSpec) {
446+
for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) {
447+
propagateDemandControlDirectives(type, subgraphType, subgraph, args.costSpec);
448+
}
455449
}
456450

457451
for (const field of type.fields()) {
@@ -460,7 +454,13 @@ function extractObjOrItfContent(args: ExtractArguments, info: TypeInfo<ObjectTyp
460454
// In fed2 subgraph, no @join__field means that the field is in all the subgraphs in which the type is.
461455
const isShareable = isObjectType(type) && subgraphsInfo.size > 1;
462456
for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) {
463-
addSubgraphField({ field, type: subgraphType, subgraph, isShareable, originalDirectiveNames });
457+
addSubgraphField({
458+
field,
459+
type: subgraphType,
460+
subgraph,
461+
isShareable,
462+
costSpec: args.costSpec
463+
});
464464
}
465465
} else {
466466
const isShareable = isObjectType(type)
@@ -478,78 +478,52 @@ function extractObjOrItfContent(args: ExtractArguments, info: TypeInfo<ObjectTyp
478478
}
479479

480480
const { type: subgraphType, subgraph } = subgraphsInfo.get(joinFieldArgs.graph)!;
481-
addSubgraphField({ field, type: subgraphType, subgraph, isShareable, joinFieldArgs, originalDirectiveNames });
482-
}
483-
}
484-
}
485-
}
486-
}
487-
488-
/**
489-
* Builds a map of original name to new name for Apollo feature directives. This is
490-
* used to handle cases where a directive is renamed via an import statement. For
491-
* example, importing a directive with a custom name like
492-
* ```graphql
493-
* @link(url: "https://specs.apollo.dev/cost/v0.1", import: [{ name: "@cost", as: "@renamedCost" }])
494-
* ```
495-
* results in a map entry of `cost -> renamedCost` with the `@` prefix removed.
496-
*
497-
* If the directive is imported under its default name, that also results in an entry. So,
498-
* ```graphql
499-
* @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@cost"])
500-
* ```
501-
* results in a map entry of `cost -> cost`. This duals as a way to check if a directive
502-
* is included in the supergraph schema.
503-
*
504-
* **Important:** This map does _not_ include directives imported from identities other
505-
* than `specs.apollo.dev`. This helps us avoid extracting directives to subgraphs
506-
* when a custom directive's name conflicts with that of a default one.
507-
*/
508-
function getApolloDirectiveNames(supergraph: Schema): Record<string, string> {
509-
const originalDirectiveNames: Record<string, string> = {};
510-
for (const linkDirective of supergraph.schemaDefinition.appliedDirectivesOf("link")) {
511-
if (linkDirective.arguments().url && linkDirective.arguments().import) {
512-
const url = FeatureUrl.maybeParse(linkDirective.arguments().url);
513-
if (!url?.identity.includes("specs.apollo.dev")) {
514-
continue;
515-
}
516-
517-
for (const importedDirective of linkDirective.arguments().import) {
518-
if (importedDirective.name && importedDirective.as) {
519-
originalDirectiveNames[importedDirective.name.replace('@', '')] = importedDirective.as.replace('@', '');
520-
} else if (typeof importedDirective === 'string') {
521-
originalDirectiveNames[importedDirective.replace('@', '')] = importedDirective.replace('@', '');
481+
addSubgraphField({
482+
field,
483+
type: subgraphType,
484+
subgraph, isShareable,
485+
joinFieldArgs,
486+
costSpec: args.costSpec
487+
});
522488
}
523489
}
524490
}
525491
}
526-
527-
return originalDirectiveNames;
528492
}
529493

530494
function extractInputObjContent(args: ExtractArguments, info: TypeInfo<InputObjectType>[]) {
531495
const fieldDirective = args.joinSpec.fieldDirective(args.supergraph);
532-
const originalDirectiveNames = args.originalDirectiveNames;
533496

534497
for (const { type, subgraphsInfo } of info) {
535498
for (const field of type.fields()) {
536499
const fieldApplications = field.appliedDirectivesOf(fieldDirective);
537500
if (fieldApplications.length === 0) {
538501
// In fed2 subgraph, no @join__field means that the field is in all the subgraphs in which the type is.
539502
for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) {
540-
addSubgraphInputField({ field, type: subgraphType, subgraph, originalDirectiveNames });
503+
addSubgraphInputField({
504+
field,
505+
type: subgraphType,
506+
subgraph,
507+
costSpec: args.costSpec
508+
});
541509
}
542510
} else {
543511
for (const application of fieldApplications) {
544-
const args = application.arguments();
512+
const joinFieldArgs = application.arguments();
545513
// We use a @join__field with no graph to indicates when a field in the supergraph does not come
546514
// directly from any subgraph and there is thus nothing to do to "extract" it.
547-
if (!args.graph) {
515+
if (!joinFieldArgs.graph) {
548516
continue;
549517
}
550518

551-
const { type: subgraphType, subgraph } = subgraphsInfo.get(args.graph)!;
552-
addSubgraphInputField({ field, type: subgraphType, subgraph, joinFieldArgs: args, originalDirectiveNames });
519+
const { type: subgraphType, subgraph } = subgraphsInfo.get(joinFieldArgs.graph)!;
520+
addSubgraphInputField({
521+
field,
522+
type: subgraphType,
523+
subgraph,
524+
joinFieldArgs,
525+
costSpec: args.costSpec
526+
});
553527
}
554528
}
555529
}
@@ -559,11 +533,12 @@ function extractInputObjContent(args: ExtractArguments, info: TypeInfo<InputObje
559533
function extractEnumTypeContent(args: ExtractArguments, info: TypeInfo<EnumType>[]) {
560534
// This was added in join 0.3, so it can genuinely be undefined.
561535
const enumValueDirective = args.joinSpec.enumValueDirective(args.supergraph);
562-
const originalDirectiveNames = args.originalDirectiveNames;
563536

564537
for (const { type, subgraphsInfo } of info) {
565-
for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) {
566-
propagateDemandControlDirectives(type, subgraphType, subgraph, originalDirectiveNames);
538+
if (args.costSpec) {
539+
for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) {
540+
propagateDemandControlDirectives(type, subgraphType, subgraph, args.costSpec);
541+
}
567542
}
568543

569544
for (const value of type.values) {
@@ -678,20 +653,20 @@ function maybeDumpSubgraphSchema(subgraph: Subgraph): string {
678653
}
679654
}
680655

681-
function propagateDemandControlDirectives(source: SchemaElement<any, any>, dest: SchemaElement<any, any>, subgraph: Subgraph, originalDirectiveNames?: Record<string, string>) {
682-
const costDirectiveName = originalDirectiveNames?.[FederationDirectiveName.COST];
683-
if (costDirectiveName) {
684-
const costDirective = source.appliedDirectivesOf(costDirectiveName).pop();
685-
if (costDirective) {
686-
dest.applyDirective(subgraph.metadata().costDirective().name, costDirective.arguments());
656+
function propagateDemandControlDirectives(source: SchemaElement<any, any>, dest: SchemaElement<any, any>, subgraph: Subgraph, costSpec: CostSpecDefinition) {
657+
const costDirective = costSpec.costDirective(source.schema());
658+
if (costDirective) {
659+
const application = source.appliedDirectivesOf(costDirective)[0];
660+
if (application) {
661+
dest.applyDirective(subgraph.metadata().costDirective().name, application.arguments());
687662
}
688663
}
689664

690-
const listSizeDirectiveName = originalDirectiveNames?.[FederationDirectiveName.LIST_SIZE];
691-
if (listSizeDirectiveName) {
692-
const listSizeDirective = source.appliedDirectivesOf(listSizeDirectiveName).pop();
693-
if (listSizeDirective) {
694-
dest.applyDirective(subgraph.metadata().listSizeDirective().name, listSizeDirective.arguments());
665+
const listSizeDirective = costSpec.listSizeDirective(source.schema());
666+
if (listSizeDirective) {
667+
const application = source.appliedDirectivesOf(listSizeDirective)[0];
668+
if (application) {
669+
dest.applyDirective(subgraph.metadata().listSizeDirective().name, application.arguments());
695670
}
696671
}
697672
}
@@ -707,14 +682,14 @@ function addSubgraphField({
707682
subgraph,
708683
isShareable,
709684
joinFieldArgs,
710-
originalDirectiveNames,
685+
costSpec,
711686
}: {
712687
field: FieldDefinition<ObjectType | InterfaceType>,
713688
type: ObjectType | InterfaceType,
714689
subgraph: Subgraph,
715690
isShareable: boolean,
716691
joinFieldArgs?: JoinFieldDirectiveArguments,
717-
originalDirectiveNames?: Record<string, string>,
692+
costSpec?: CostSpecDefinition,
718693
}): FieldDefinition<ObjectType | InterfaceType> {
719694
const copiedFieldType = joinFieldArgs?.type
720695
? decodeType(joinFieldArgs.type, subgraph.schema, subgraph.name)
@@ -723,7 +698,9 @@ function addSubgraphField({
723698
const subgraphField = type.addField(field.name, copiedFieldType);
724699
for (const arg of field.arguments()) {
725700
const argDef = subgraphField.addArgument(arg.name, copyType(arg.type!, subgraph.schema, subgraph.name), arg.defaultValue);
726-
propagateDemandControlDirectives(arg, argDef, subgraph, originalDirectiveNames)
701+
if (costSpec) {
702+
propagateDemandControlDirectives(arg, argDef, subgraph, costSpec);
703+
}
727704
}
728705
if (joinFieldArgs?.requires) {
729706
subgraphField.applyDirective(subgraph.metadata().requiresDirective(), {'fields': joinFieldArgs.requires});
@@ -769,7 +746,9 @@ function addSubgraphField({
769746
subgraphField.applyDirective(subgraph.metadata().shareableDirective());
770747
}
771748

772-
propagateDemandControlDirectives(field, subgraphField, subgraph, originalDirectiveNames);
749+
if (costSpec) {
750+
propagateDemandControlDirectives(field, subgraphField, subgraph, costSpec);
751+
}
773752

774753
return subgraphField;
775754
}
@@ -779,13 +758,13 @@ function addSubgraphInputField({
779758
type,
780759
subgraph,
781760
joinFieldArgs,
782-
originalDirectiveNames,
761+
costSpec,
783762
}: {
784763
field: InputFieldDefinition,
785764
type: InputObjectType,
786765
subgraph: Subgraph,
787766
joinFieldArgs?: JoinFieldDirectiveArguments,
788-
originalDirectiveNames?: Record<string, string>
767+
costSpec?: CostSpecDefinition,
789768
}): InputFieldDefinition {
790769
const copiedType = joinFieldArgs?.type
791770
? decodeType(joinFieldArgs?.type, subgraph.schema, subgraph.name)
@@ -794,7 +773,9 @@ function addSubgraphInputField({
794773
const inputField = type.addField(field.name, copiedType);
795774
inputField.defaultValue = field.defaultValue
796775

797-
propagateDemandControlDirectives(field, inputField, subgraph, originalDirectiveNames);
776+
if (costSpec) {
777+
propagateDemandControlDirectives(field, inputField, subgraph, costSpec);
778+
}
798779

799780
return inputField;
800781
}

internals-js/src/specs/costSpec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { DirectiveLocation } from 'graphql';
22
import { createDirectiveSpecification } from '../directiveAndTypeSpecification';
33
import { FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion } from './coreSpec';
4-
import { ListType, NonNullType } from '../definitions';
4+
import { DirectiveDefinition, ListType, NonNullType, Schema } from '../definitions';
55
import { registerKnownFeature } from '../knownCoreFeatures';
66
import { ARGUMENT_COMPOSITION_STRATEGIES } from '../argumentCompositionStrategies';
77

@@ -41,6 +41,14 @@ export class CostSpecDefinition extends FeatureDefinition {
4141
supergraphSpecification: (fedVersion) => COST_VERSIONS.getMinimumRequiredVersion(fedVersion)
4242
}));
4343
}
44+
45+
costDirective(schema: Schema): DirectiveDefinition<CostDirectiveArguments> | undefined {
46+
return this.directive(schema, 'cost');
47+
}
48+
49+
listSizeDirective(schema: Schema): DirectiveDefinition<ListSizeDirectiveArguments> | undefined {
50+
return this.directive(schema, 'listSize');
51+
}
4452
}
4553

4654
export const COST_VERSIONS = new FeatureDefinitions<CostSpecDefinition>(costIdentity)

0 commit comments

Comments
 (0)