Skip to content

Commit fc8224d

Browse files
committed
Some bugfixes to latest changes for 2.9.0, along with a few bugfixes for older code
1 parent 351f80c commit fc8224d

11 files changed

Lines changed: 385 additions & 254 deletions

File tree

composition-js/src/__tests__/compose.directiveArgumentMergeStrategies.test.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,19 @@ describe('composition of directive with non-trivial argument strategies', () =>
8989
t: 2, k: 1, b: 4,
9090
},
9191
}, {
92-
name: 'sum',
93-
type: (schema: Schema) => new NonNullType(schema.intType()),
94-
compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.SUM,
95-
argValues: {
96-
s1: { t: 3, k: 1 },
97-
s2: { t: 2, k: 5, b: 4 },
98-
},
99-
resultValues: {
100-
t: 5, k: 6, b: 4,
101-
},
102-
}, {
92+
// NOTE: See the note for the SUM strategy in argumentCompositionStrategies.ts
93+
// for more information on why this is commented out.
94+
// name: 'sum',
95+
// type: (schema: Schema) => new NonNullType(schema.intType()),
96+
// compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.SUM,
97+
// argValues: {
98+
// s1: { t: 3, k: 1 },
99+
// s2: { t: 2, k: 5, b: 4 },
100+
// },
101+
// resultValues: {
102+
// t: 5, k: 6, b: 4,
103+
// },
104+
// }, {
103105
name: 'intersection',
104106
type: (schema: Schema) => new NonNullType(new ListType(new NonNullType(schema.stringType()))),
105107
compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.INTERSECTION,
@@ -219,7 +221,7 @@ describe('composition of directive with non-trivial argument strategies', () =>
219221
const s = result.schema;
220222

221223
expect(directiveStrings(s.schemaDefinition, name)).toStrictEqual([
222-
`@link(url: "https://specs.apollo.dev/${name}/v0.1", import: ["@${name}"])`
224+
`@link(url: "https://specs.apollo.dev/${name}/v0.1")`
223225
]);
224226

225227
const t = s.type('T') as ObjectType;

composition-js/src/merging/merge.ts

Lines changed: 131 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import {
3131
CompositeType,
3232
Subgraphs,
3333
JOIN_VERSIONS,
34-
INACCESSIBLE_VERSIONS,
3534
NamedSchemaElement,
3635
errorCauses,
3736
isObjectType,
@@ -69,7 +68,6 @@ import {
6968
CoreSpecDefinition,
7069
FeatureVersion,
7170
FEDERATION_VERSIONS,
72-
InaccessibleSpecDefinition,
7371
LinkDirectiveArgs,
7472
sourceIdentity,
7573
FeatureUrl,
@@ -81,6 +79,10 @@ import {
8179
isNullableType,
8280
isFieldDefinition,
8381
Post20FederationDirectiveDefinition,
82+
DirectiveCompositionSpecification,
83+
FeatureDefinition,
84+
CoreImport,
85+
inaccessibleIdentity,
8486
} from "@apollo/federation-internals";
8587
import { ASTNode, GraphQLError, DirectiveLocation } from "graphql";
8688
import {
@@ -345,6 +347,12 @@ interface OverrideArgs {
345347
label?: string;
346348
}
347349

350+
interface MergedDirectiveInfo {
351+
definition: DirectiveDefinition;
352+
argumentsMerger?: ArgumentMerger;
353+
staticArgumentTransform?: StaticArgumentsTransform;
354+
}
355+
348356
class Merger {
349357
readonly names: readonly string[];
350358
readonly subgraphsSchema: readonly Schema[];
@@ -353,7 +361,8 @@ class Merger {
353361
readonly merged: Schema = new Schema();
354362
readonly subgraphNamesToJoinSpecName: Map<string, string>;
355363
readonly mergedFederationDirectiveNames = new Set<string>();
356-
readonly mergedFederationDirectiveInSupergraph = new Map<string, { definition: DirectiveDefinition, argumentsMerger?: ArgumentMerger, staticArgumentTransform?: StaticArgumentsTransform }>();
364+
readonly mergedFederationDirectiveInSupergraphByDirectiveName =
365+
new Map<string, MergedDirectiveInfo>();
357366
readonly enumUsages = new Map<string, EnumTypeUsage>();
358367
private composeDirectiveManager: ComposeDirectiveManager;
359368
private mismatchReporter: MismatchReporter;
@@ -364,7 +373,7 @@ class Merger {
364373
}[];
365374
private joinSpec: JoinSpecDefinition;
366375
private linkSpec: CoreSpecDefinition;
367-
private inaccessibleSpec: InaccessibleSpecDefinition;
376+
private inaccessibleDirectiveInSupergraph?: DirectiveDefinition;
368377
private latestFedVersionUsed: FeatureVersion;
369378
private joinDirectiveIdentityURLs = new Set<string>();
370379
private schemaToImportNameToFeatureUrl = new Map<Schema, Map<string, FeatureUrl>>();
@@ -375,7 +384,6 @@ class Merger {
375384
this.latestFedVersionUsed = this.getLatestFederationVersionUsed();
376385
this.joinSpec = JOIN_VERSIONS.getMinimumRequiredVersion(this.latestFedVersionUsed);
377386
this.linkSpec = LINK_VERSIONS.getMinimumRequiredVersion(this.latestFedVersionUsed);
378-
this.inaccessibleSpec = INACCESSIBLE_VERSIONS.getMinimumRequiredVersion(this.latestFedVersionUsed);
379387
this.fieldsWithFromContext = this.getFieldsWithFromContextDirective();
380388
this.fieldsWithOverride = this.getFieldsWithOverrideDirective();
381389

@@ -470,59 +478,127 @@ class Merger {
470478
assert(errors.length === 0, "We shouldn't have errors adding the join spec to the (still empty) supergraph schema");
471479

472480
const directivesMergeInfo = collectCoreDirectivesToCompose(this.subgraphs);
473-
for (const mergeInfo of directivesMergeInfo) {
474-
this.validateAndMaybeAddSpec(mergeInfo);
475-
}
476-
481+
this.validateAndMaybeAddSpecs(directivesMergeInfo);
477482
return this.joinSpec.populateGraphEnum(this.merged, this.subgraphs);
478483
}
479484

480-
private validateAndMaybeAddSpec({url, name, definitionsPerSubgraph, compositionSpec}: CoreDirectiveInSubgraphs) {
481-
// Not composition specification means that it shouldn't be composed.
482-
if (!compositionSpec) {
483-
return;
484-
}
485-
486-
let nameInSupergraph: string | undefined;
487-
for (const subgraph of this.subgraphs) {
488-
const directive = definitionsPerSubgraph.get(subgraph.name);
489-
if (!directive) {
490-
continue;
485+
private validateAndMaybeAddSpecs(directivesMergeInfo: CoreDirectiveInSubgraphs[]) {
486+
const supergraphInfoByIdentity = new Map<
487+
string,
488+
{
489+
specInSupergraph: FeatureDefinition;
490+
directives: {
491+
nameInFeature: string;
492+
nameInSupergraph: string;
493+
compositionSpec: DirectiveCompositionSpecification;
494+
}[];
491495
}
496+
>;
492497

493-
if (!nameInSupergraph) {
494-
nameInSupergraph = directive.name;
495-
} else if (nameInSupergraph !== directive.name) {
496-
this.mismatchReporter.reportMismatchError(
497-
ERRORS.LINK_IMPORT_NAME_MISMATCH,
498-
`The "@${name}" directive (from ${url}) is imported with mismatched name between subgraphs: it is imported as `,
499-
directive,
500-
sourcesFromArray(this.subgraphs.values().map((s) => definitionsPerSubgraph.get(s.name))),
501-
(def) => `"@${def.name}"`,
502-
);
498+
for (const {url, name, definitionsPerSubgraph, compositionSpec} of directivesMergeInfo) {
499+
// No composition specification means that it shouldn't be composed.
500+
if (!compositionSpec) {
503501
return;
504502
}
503+
504+
let nameInSupergraph: string | undefined;
505+
for (const subgraph of this.subgraphs) {
506+
const directive = definitionsPerSubgraph.get(subgraph.name);
507+
if (!directive) {
508+
continue;
509+
}
510+
511+
if (!nameInSupergraph) {
512+
nameInSupergraph = directive.name;
513+
} else if (nameInSupergraph !== directive.name) {
514+
this.mismatchReporter.reportMismatchError(
515+
ERRORS.LINK_IMPORT_NAME_MISMATCH,
516+
`The "@${name}" directive (from ${url}) is imported with mismatched name between subgraphs: it is imported as `,
517+
directive,
518+
sourcesFromArray(this.subgraphs.values().map((s) => definitionsPerSubgraph.get(s.name))),
519+
(def) => `"@${def.name}"`,
520+
);
521+
return;
522+
}
523+
}
524+
525+
// If we get here with `nameInSupergraph` unset, it means there is no usage for the directive at all and we
526+
// don't bother adding the spec to the supergraph.
527+
if (nameInSupergraph) {
528+
const specInSupergraph = compositionSpec.supergraphSpecification(this.latestFedVersionUsed);
529+
let supergraphInfo = supergraphInfoByIdentity.get(specInSupergraph.url.identity);
530+
if (supergraphInfo) {
531+
assert(
532+
specInSupergraph.url.equals(supergraphInfo.specInSupergraph.url),
533+
`Spec ${specInSupergraph.url} directives disagree on version for supergraph`,
534+
);
535+
} else {
536+
supergraphInfo = {
537+
specInSupergraph,
538+
directives: [],
539+
};
540+
supergraphInfoByIdentity.set(specInSupergraph.url.identity, supergraphInfo);
541+
}
542+
supergraphInfo.directives.push({
543+
nameInFeature: name,
544+
nameInSupergraph,
545+
compositionSpec,
546+
});
547+
}
505548
}
506549

507-
// If we get here with `nameInSupergraph` unset, it means there is no usage for the directive at all and we
508-
// don't bother adding the spec to the supergraph.
509-
if (nameInSupergraph) {
510-
const specInSupergraph = compositionSpec.supergraphSpecification(this.latestFedVersionUsed);
511-
const errors = this.linkSpec.applyFeatureAsLink(this.merged, specInSupergraph, specInSupergraph.defaultCorePurpose, [{ name, as: name === nameInSupergraph ? undefined : nameInSupergraph }], );
512-
assert(errors.length === 0, "We shouldn't have errors adding the join spec to the (still empty) supergraph schema");
513-
const feature = this.merged?.coreFeatures?.getByIdentity(specInSupergraph.url.identity);
550+
for (const { specInSupergraph, directives } of supergraphInfoByIdentity.values()) {
551+
const imports: CoreImport[] = [];
552+
for (const { nameInFeature, nameInSupergraph } of directives) {
553+
const defaultNameInSupergraph = CoreFeature.directiveNameInSchemaForCoreArguments(
554+
specInSupergraph.url,
555+
specInSupergraph.url.name,
556+
[],
557+
nameInFeature,
558+
);
559+
if (nameInSupergraph !== defaultNameInSupergraph) {
560+
imports.push(nameInFeature === nameInSupergraph
561+
? { name: `@${nameInFeature}` }
562+
: { name: `@${nameInFeature}`, as: `@${nameInSupergraph}` }
563+
);
564+
}
565+
}
566+
const errors = this.linkSpec.applyFeatureToSchema(
567+
this.merged,
568+
specInSupergraph,
569+
undefined,
570+
specInSupergraph.defaultCorePurpose,
571+
imports,
572+
);
573+
assert(
574+
errors.length === 0,
575+
"We shouldn't have errors adding the join spec to the (still empty) supergraph schema"
576+
);
577+
const feature = this.merged.coreFeatures?.getByIdentity(specInSupergraph.url.identity);
514578
assert(feature, 'Should have found the feature we just added');
515-
const argumentsMerger = compositionSpec.argumentsMerger?.call(null, this.merged, feature);
516-
if (argumentsMerger instanceof GraphQLError) {
517-
// That would mean we made a mistake in the declaration of a hard-coded directive, so we just throw right away so this can be caught and corrected.
518-
throw argumentsMerger;
519-
}
520-
this.mergedFederationDirectiveNames.add(nameInSupergraph);
521-
this.mergedFederationDirectiveInSupergraph.set(name, {
522-
definition: this.merged.directive(nameInSupergraph)!,
523-
argumentsMerger,
524-
staticArgumentTransform: compositionSpec.staticArgumentTransform,
525-
});
579+
for (const { nameInFeature, nameInSupergraph, compositionSpec } of directives) {
580+
const argumentsMerger = compositionSpec.argumentsMerger?.call(null, this.merged, feature);
581+
if (argumentsMerger instanceof GraphQLError) {
582+
// That would mean we made a mistake in the declaration of a hard-coded directive,
583+
// so we just throw right away so this can be caught and corrected.
584+
throw argumentsMerger;
585+
}
586+
this.mergedFederationDirectiveNames.add(nameInSupergraph);
587+
this.mergedFederationDirectiveInSupergraphByDirectiveName.set(nameInSupergraph, {
588+
definition: this.merged.directive(nameInSupergraph)!,
589+
argumentsMerger,
590+
staticArgumentTransform: compositionSpec.staticArgumentTransform,
591+
});
592+
// If we encounter the @inaccessible directive, we need to record its
593+
// definition so certain merge validations that care about @inaccessible
594+
// can act accordingly.
595+
if (
596+
specInSupergraph.identity === inaccessibleIdentity
597+
&& nameInFeature === specInSupergraph.url.name
598+
) {
599+
this.inaccessibleDirectiveInSupergraph = this.merged.directive(nameInSupergraph)!;
600+
}
601+
}
526602
}
527603
}
528604

@@ -2464,8 +2540,8 @@ class Merger {
24642540
this.recordAppliedDirectivesToMerge(valueSources, value);
24652541
this.addJoinEnumValue(valueSources, value);
24662542

2467-
const inaccessibleInSupergraph = this.mergedFederationDirectiveInSupergraph.get(this.inaccessibleSpec.inaccessibleDirectiveSpec.name);
2468-
const isInaccessible = inaccessibleInSupergraph && value.hasAppliedDirective(inaccessibleInSupergraph.definition);
2543+
const isInaccessible = this.inaccessibleDirectiveInSupergraph
2544+
&& value.hasAppliedDirective(this.inaccessibleDirectiveInSupergraph);
24692545
// The merging strategy depends on the enum type usage:
24702546
// - if it is _only_ used in position of Input type, we merge it with an "intersection" strategy (like other input types/things).
24712547
// - if it is _only_ used in position of Output type, we merge it with an "union" strategy (like other output types/things).
@@ -2562,8 +2638,6 @@ class Merger {
25622638
}
25632639

25642640
private mergeInput(inputSources: Sources<InputObjectType>, dest: InputObjectType) {
2565-
const inaccessibleInSupergraph = this.mergedFederationDirectiveInSupergraph.get(this.inaccessibleSpec.inaccessibleDirectiveSpec.name);
2566-
25672641
// Like for other inputs, we add all the fields found in any subgraphs initially as a simple mean to have a complete list of
25682642
// field to iterate over, but we will remove those that are not in all subgraphs.
25692643
const added = this.addFieldsShallow(inputSources, dest);
@@ -2572,7 +2646,8 @@ class Merger {
25722646
// compatibility between definitions and 2) we actually want to see if the result is marked inaccessible or not and it makes
25732647
// that easier.
25742648
this.mergeInputField(subgraphFields, destField);
2575-
const isInaccessible = inaccessibleInSupergraph && destField.hasAppliedDirective(inaccessibleInSupergraph.definition);
2649+
const isInaccessible = this.inaccessibleDirectiveInSupergraph
2650+
&& destField.hasAppliedDirective(this.inaccessibleDirectiveInSupergraph);
25762651
// Note that if the field is manually marked @inaccessible, we can always accept it to be inconsistent between subgraphs since
25772652
// it won't be exposed in the API, and we don't hint about it because we're just doing what the user is explicitely asking.
25782653
if (!isInaccessible && someSources(subgraphFields, field => !field)) {
@@ -2840,8 +2915,7 @@ class Merger {
28402915
// is @inaccessible, which is necessary to exist in the supergraph for EnumValues to properly
28412916
// determine whether the fact that a value is both input / output will matter
28422917
private recordAppliedDirectivesToMerge(sources: Sources<SchemaElement<any, any>>, dest: SchemaElement<any, any>) {
2843-
const inaccessibleInSupergraph = this.mergedFederationDirectiveInSupergraph.get(this.inaccessibleSpec.inaccessibleDirectiveSpec.name);
2844-
const inaccessibleName = inaccessibleInSupergraph?.definition.name;
2918+
const inaccessibleName = this.inaccessibleDirectiveInSupergraph?.name;
28452919
const names = this.gatherAppliedDirectiveNames(sources);
28462920

28472921
if (inaccessibleName && names.has(inaccessibleName)) {
@@ -2905,7 +2979,7 @@ class Merger {
29052979
return;
29062980
}
29072981

2908-
const directiveInSupergraph = this.mergedFederationDirectiveInSupergraph.get(name);
2982+
const directiveInSupergraph = this.mergedFederationDirectiveInSupergraphByDirectiveName.get(name);
29092983

29102984
if (dest.schema().directive(name)?.repeatable) {
29112985
// For repeatable directives, we simply include each application found but with exact duplicates removed
@@ -2945,7 +3019,7 @@ class Merger {
29453019
if (differentApplications.length === 1) {
29463020
dest.applyDirective(name, differentApplications[0].arguments(false));
29473021
} else {
2948-
const info = this.mergedFederationDirectiveInSupergraph.get(name);
3022+
const info = this.mergedFederationDirectiveInSupergraphByDirectiveName.get(name);
29493023
if (info && info.argumentsMerger) {
29503024
const mergedArguments = Object.create(null);
29513025
const applicationsArguments = differentApplications.map((a) => a.arguments(true));

gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ describe('lifecycle hooks', () => {
149149
// the supergraph (even just formatting differences), this ID will change
150150
// and this test will have to updated.
151151
expect(secondCall[0]!.compositionId).toMatchInlineSnapshot(
152-
`"4aa2278e35df345ff5959a30546d2e9ef9e997204b4ffee4a42344b578b36068"`,
152+
`"6dc1bde2b9818fabec62208c5d8825abaa1bae89635fa6f3a5ffea7b78fc6d82"`,
153153
);
154154
// second call should have previous info in the second arg
155155
expect(secondCall[1]!.compositionId).toEqual(expectedFirstId);

internals-js/src/__tests__/values.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,5 +414,6 @@ describe('objectEquals tests', () => {
414414
expect(valueEquals({ foo: 'foo', bar: undefined }, { foo: 'foo' })).toBe(
415415
false,
416416
);
417+
expect(valueEquals({}, null)).toBe(false);
417418
});
418419
});

0 commit comments

Comments
 (0)