Skip to content

Commit a812d8d

Browse files
committed
Validate against directive arguments
Custom executable directives should be validated as equal only if they have the same locations and same arguments.
1 parent 0e4ebbe commit a812d8d

File tree

2 files changed

+62
-3
lines changed

2 files changed

+62
-3
lines changed

packages/apollo-federation/src/composition/utils.ts

+29-2
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,10 @@ export function diffTypeNodes(
365365

366366
const locationsDiff: Set<string> = new Set();
367367

368+
const argumentsDiff: {
369+
[argumentName: string]: string[];
370+
} = Object.create(null);
371+
368372
const document: DocumentNode = {
369373
kind: Kind.DOCUMENT,
370374
definitions: [firstNode, secondNode],
@@ -416,6 +420,27 @@ export function diffTypeNodes(
416420
locationsDiff.add(locationName);
417421
}
418422
});
423+
424+
if (!node.arguments) return;
425+
426+
// Arguments must have the same name and type. As matches are found, they
427+
// are deleted from the diff. Anything left in the diff after looping
428+
// represents a discrepancy between the two sets of arguments.
429+
node.arguments.forEach(argument => {
430+
const argumentName = argument.name.value;
431+
const printedType = print(argument.type);
432+
if (argumentsDiff[argumentName]) {
433+
if (printedType === argumentsDiff[argumentName][0]) {
434+
// If the existing entry is equal to printedType, it means there's no
435+
// diff, so we can remove the entry from the diff object
436+
delete argumentsDiff[argumentName];
437+
} else {
438+
argumentsDiff[argumentName].push(printedType);
439+
}
440+
} else {
441+
argumentsDiff[argumentName] = [printedType];
442+
}
443+
});
419444
},
420445
});
421446

@@ -433,6 +458,7 @@ export function diffTypeNodes(
433458
fields: fieldsDiff,
434459
unionTypes: unionTypesDiff,
435460
locations: Array.from(locationsDiff),
461+
args: argumentsDiff,
436462
};
437463
}
438464

@@ -446,7 +472,7 @@ export function typeNodesAreEquivalent(
446472
firstNode: TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode,
447473
secondNode: TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode,
448474
) {
449-
const { name, kind, fields, unionTypes, locations } = diffTypeNodes(
475+
const { name, kind, fields, unionTypes, locations, args } = diffTypeNodes(
450476
firstNode,
451477
secondNode,
452478
);
@@ -456,7 +482,8 @@ export function typeNodesAreEquivalent(
456482
kind.length === 0 &&
457483
Object.keys(fields).length === 0 &&
458484
Object.keys(unionTypes).length === 0 &&
459-
locations.length === 0
485+
locations.length === 0 &&
486+
Object.keys(args).length === 0
460487
);
461488
}
462489

packages/apollo-federation/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts

+33-1
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ describe('executableDirectivesIdentical', () => {
1010
const serviceA = {
1111
typeDefs: gql`
1212
directive @stream on FIELD
13+
directive @instrument(tag: String!) on FIELD
1314
`,
1415
name: 'serviceA',
1516
};
1617

1718
const serviceB = {
1819
typeDefs: gql`
1920
directive @stream on FIELD
21+
directive @instrument(tag: String!) on FIELD
2022
`,
2123
name: 'serviceB',
2224
};
@@ -27,7 +29,7 @@ describe('executableDirectivesIdentical', () => {
2729
expect(errors).toHaveLength(0);
2830
});
2931

30-
it("throws errors when custom, executable directives aren't defined in every service", () => {
32+
it("throws errors when custom, executable directives aren't defined with the same locations in every service", () => {
3133
const serviceA = {
3234
typeDefs: gql`
3335
directive @stream on FIELD
@@ -64,4 +66,34 @@ describe('executableDirectivesIdentical', () => {
6466
]
6567
`);
6668
});
69+
70+
it("throws errors when custom, executable directives aren't defined with the same arguments in every service", () => {
71+
const serviceA = {
72+
typeDefs: gql`
73+
directive @instrument(tag: String!) on FIELD
74+
`,
75+
name: 'serviceA',
76+
};
77+
78+
const serviceB = {
79+
typeDefs: gql`
80+
directive @instrument(tag: Boolean) on FIELD
81+
`,
82+
name: 'serviceB',
83+
};
84+
85+
const serviceList = [serviceA, serviceB];
86+
const { schema } = composeServices(serviceList);
87+
const errors = executableDirectivesIdentical({ schema, serviceList });
88+
expect(errors).toMatchInlineSnapshot(`
89+
Array [
90+
Object {
91+
"code": "EXECUTABLE_DIRECTIVES_IDENTICAL",
92+
"message": "[@instrument] -> custom directives must be defined identically across all services. See below for a list of current implementations:
93+
serviceA: directive @instrument(tag: String!) on FIELD
94+
serviceB: directive @instrument(tag: Boolean) on FIELD",
95+
},
96+
]
97+
`);
98+
});
6799
});

0 commit comments

Comments
 (0)