Skip to content

Commit 8ff04c5

Browse files
authored
fix: dont touch external fields or objects in contract logic (#6903)
1 parent abf8af6 commit 8ff04c5

File tree

2 files changed

+202
-0
lines changed

2 files changed

+202
-0
lines changed

integration-tests/tests/schema/contracts.spec.ts

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,3 +479,191 @@ test('inaccessible is not applied on type if at least one type extension has a p
479479
expect(result.contracts?.[0].errors).toEqual([]);
480480
expect(result.contracts?.[0].sdl).toContain('type Brr {');
481481
});
482+
483+
test('@external fields are always included if exclude @tag is used', async () => {
484+
const result = await client.composeAndValidate.mutate({
485+
type: 'federation',
486+
native: true,
487+
schemas: [
488+
{
489+
raw: /* GraphQL */ `
490+
extend schema
491+
@link(url: "https://specs.apollo.dev/link/v1.0")
492+
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@key", "@tag"])
493+
494+
type Query {
495+
user: User
496+
}
497+
498+
type User @key(fields: "id") {
499+
id: ID!
500+
ssn: String @tag(name: "private")
501+
}
502+
`,
503+
source: 'user.graphql',
504+
url: 'https://localhost:3000/graphql',
505+
},
506+
{
507+
raw: /* GraphQL */ `
508+
extend schema
509+
@link(url: "https://specs.apollo.dev/link/v1.0")
510+
@link(
511+
url: "https://specs.apollo.dev/federation/v2.8"
512+
import: ["@key", "@external", "@requires"]
513+
)
514+
515+
type User @key(fields: "id") {
516+
id: ID!
517+
ssn: String @external
518+
creditScore: Int @requires(fields: "ssn")
519+
}
520+
`,
521+
source: 'credit.graphql',
522+
url: 'https://localhost:3001/graphql',
523+
},
524+
],
525+
external: null,
526+
contracts: [
527+
{
528+
id: 'foo',
529+
filter: {
530+
removeUnreachableTypesFromPublicApiSchema: false,
531+
exclude: ['private'],
532+
include: null,
533+
},
534+
},
535+
],
536+
});
537+
538+
expect(result.errors).toEqual([]);
539+
expect(result.contracts?.[0].errors).toEqual([]);
540+
expect(result.contracts?.[0].sdl).toMatchInlineSnapshot(`
541+
type User {
542+
id: ID!
543+
creditScore: Int
544+
}
545+
546+
type Query {
547+
user: User
548+
}
549+
`);
550+
});
551+
552+
test('@external fields are always included if include @tag is used', async () => {
553+
const result = await client.composeAndValidate.mutate({
554+
type: 'federation',
555+
native: true,
556+
schemas: [
557+
{
558+
raw: /* GraphQL */ `
559+
extend schema
560+
@link(url: "https://specs.apollo.dev/link/v1.0")
561+
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@key", "@tag"])
562+
563+
type Query {
564+
user: User @tag(name: "public")
565+
}
566+
567+
type User @key(fields: "id") {
568+
id: ID! @tag(name: "public")
569+
ssn: String
570+
}
571+
`,
572+
source: 'user.graphql',
573+
url: 'https://localhost:3000/graphql',
574+
},
575+
{
576+
raw: /* GraphQL */ `
577+
extend schema
578+
@link(url: "https://specs.apollo.dev/link/v1.0")
579+
@link(
580+
url: "https://specs.apollo.dev/federation/v2.8"
581+
import: ["@key", "@external", "@requires", "@tag"]
582+
)
583+
584+
type User @key(fields: "id") {
585+
id: ID!
586+
ssn: String @external
587+
creditScore: Int @requires(fields: "ssn") @tag(name: "public")
588+
}
589+
`,
590+
source: 'credit.graphql',
591+
url: 'https://localhost:3001/graphql',
592+
},
593+
],
594+
external: null,
595+
contracts: [
596+
{
597+
id: 'foo',
598+
filter: {
599+
removeUnreachableTypesFromPublicApiSchema: false,
600+
exclude: null,
601+
include: ['public'],
602+
},
603+
},
604+
],
605+
});
606+
607+
expect(result.errors).toEqual([]);
608+
expect(result.contracts?.[0].errors).toEqual([]);
609+
expect(result.contracts?.[0].sdl).toMatchInlineSnapshot(`
610+
type User {
611+
id: ID!
612+
creditScore: Int
613+
}
614+
615+
type Query {
616+
user: User
617+
}
618+
`);
619+
});
620+
621+
test('include with exclude is possible', async () => {
622+
const result = await client.composeAndValidate.mutate({
623+
type: 'federation',
624+
native: true,
625+
schemas: [
626+
{
627+
raw: /* GraphQL */ `
628+
extend schema
629+
@link(url: "https://specs.apollo.dev/link/v1.0")
630+
@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@key", "@tag"])
631+
632+
type Query {
633+
user: User @tag(name: "public")
634+
}
635+
636+
type User @key(fields: "id") @tag(name: "public") {
637+
id: ID!
638+
ssn: String @tag(name: "private")
639+
}
640+
`,
641+
source: 'user.graphql',
642+
url: 'https://localhost:3000/graphql',
643+
},
644+
],
645+
external: null,
646+
contracts: [
647+
{
648+
id: 'foo',
649+
filter: {
650+
removeUnreachableTypesFromPublicApiSchema: false,
651+
exclude: ['private'],
652+
include: ['public'],
653+
},
654+
},
655+
],
656+
});
657+
658+
expect(result.errors).toEqual([]);
659+
expect(result.contracts?.[0].errors).toEqual([]);
660+
expect(result.contracts?.[0].sdl).toMatchInlineSnapshot(`
661+
type Query {
662+
user: User
663+
}
664+
665+
type User {
666+
id: ID!
667+
}
668+
`);
669+
});

packages/services/schema/src/lib/federation-tag-extraction.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ export function applyTagFilterToInaccessibleTransformOnSubgraphSchema(
111111
'@inaccessible',
112112
);
113113
const tagDirectiveName = resolveImportName('https://specs.apollo.dev/federation', '@tag');
114+
const externalDirectiveName = resolveImportName(
115+
'https://specs.apollo.dev/federation',
116+
'@external',
117+
);
114118

115119
function getTagsForSchemaCoordinate(coordinate: string) {
116120
return tagRegister.get(coordinate) ?? new Set();
@@ -140,6 +144,11 @@ export function applyTagFilterToInaccessibleTransformOnSubgraphSchema(
140144
fieldLikeNode: InputValueDefinitionNode | FieldDefinitionNode,
141145
node: InputValueDefinitionNode,
142146
) {
147+
// Check for external tag because we cannot contribute directives to external fields.
148+
if (node.directives?.find(d => d.name.value === externalDirectiveName)) {
149+
return node;
150+
}
151+
143152
const tagsOnNode = getTagsForSchemaCoordinate(
144153
`${objectLikeNode.name.value}.${fieldLikeNode.name.value}(${node.name.value}:)`,
145154
);
@@ -212,6 +221,11 @@ export function applyTagFilterToInaccessibleTransformOnSubgraphSchema(
212221
} as FieldDefinitionNode;
213222
}
214223

224+
// Check for external tag because we cannot contribute directives to external fields.
225+
if (fieldNode.directives?.find(d => d.name.value === externalDirectiveName)) {
226+
return fieldNode;
227+
}
228+
215229
if (
216230
(filter.include.size && !hasIntersection(tagsOnNode, filter.include)) ||
217231
(filter.exclude.size && hasIntersection(tagsOnNode, filter.exclude))

0 commit comments

Comments
 (0)