Skip to content

Commit

Permalink
Also validate against improper @inaccessible usage
Browse files Browse the repository at this point in the history
  • Loading branch information
trevor-scheer committed Jul 13, 2021
1 parent 884da2c commit 3d51e58
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 97 deletions.
3 changes: 2 additions & 1 deletion docs/source/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,12 @@ If Apollo Gateway encounters an error, composition fails. This document lists co
| `REQUIRES_FIELDS_MISSING_ON_BASE` | The `fields` argument of an entity field's `@requires` directive includes a field that is not defined in the entity's originating subgraph.`|
| `REQUIRES_USED_ON_BASE` | An entity field is marked with `@requires` in the entity's originating subgraph, which is invalid. |

## `@tag`
## Non-federation directives

| Code | Description |
|---|---|
| `TAG_USED_WITH_EXTERNAL` | Fields marked as `@external` cannot also have `@tag` usages. |
| `INACCESSIBLE_USED_WITH_EXTERNAL` | Fields marked as `@external` cannot also have `@inaccessible` usages. |

## Custom directives

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { tagOrInaccessibleUsedWithExternal } from '..';
import {
gql,
graphqlErrorSerializer,
} from 'apollo-federation-integration-testsuite';

expect.addSnapshotSerializer(graphqlErrorSerializer);

describe('tagOrInaccessibleUsedWithExternal', () => {
describe('no errors', () => {
it('has no errors @external and @tag are used separately', () => {
const serviceA = {
typeDefs: gql`
type Query {
product: Product @tag(name: "prod")
}
extend type Product @key(fields: "sku") {
sku: String @external
}
`,
name: 'serviceA',
};

const errors = tagOrInaccessibleUsedWithExternal(serviceA);
expect(errors).toHaveLength(0);
});

it('has no errors @external and @inaccessible are used separately', () => {
const serviceA = {
typeDefs: gql`
type Query {
product: Product @inaccessible
}
extend type Product @key(fields: "sku") {
sku: String @external
}
`,
name: 'serviceA',
};

const errors = tagOrInaccessibleUsedWithExternal(serviceA);
expect(errors).toHaveLength(0);
});
});

describe('errors', () => {
it('errors when `@external` and `@tag` are used on the same field of an object extension', () => {
const serviceA = {
typeDefs: gql`
type Query {
product: Product @tag(name: "prod")
}
extend type Product @key(fields: "sku") {
sku: String @external @tag(name: "prod")
}
`,
name: 'serviceA',
};

const errors = tagOrInaccessibleUsedWithExternal(serviceA);
expect(errors).toMatchInlineSnapshot(`
Array [
Object {
"code": "TAG_USED_WITH_EXTERNAL",
"locations": Array [
Object {
"column": 25,
"line": 7,
},
],
"message": "[serviceA] Product.sku -> Found illegal use of @tag directive. @tag directives cannot currently be used in tandem with an @external directive.",
},
]
`);
});

it('errors when `@external` and `@tag` are used on the same field of an interface extension', () => {
const serviceA = {
typeDefs: gql`
type Query {
product: Product @tag(name: "prod")
}
extend interface Product @key(fields: "sku") {
sku: String @external @tag(name: "prod")
}
`,
name: 'serviceA',
};

const errors = tagOrInaccessibleUsedWithExternal(serviceA);
expect(errors).toMatchInlineSnapshot(`
Array [
Object {
"code": "TAG_USED_WITH_EXTERNAL",
"locations": Array [
Object {
"column": 25,
"line": 7,
},
],
"message": "[serviceA] Product.sku -> Found illegal use of @tag directive. @tag directives cannot currently be used in tandem with an @external directive.",
},
]
`);
});

it('errors when `@external` and `@inaccessible` are used on the same field of an object extension', () => {
const serviceA = {
typeDefs: gql`
type Query {
product: Product @tag(name: "prod")
}
extend type Product @key(fields: "sku") {
sku: String @external @inaccessible
}
`,
name: 'serviceA',
};

const errors = tagOrInaccessibleUsedWithExternal(serviceA);
expect(errors).toMatchInlineSnapshot(`
Array [
Object {
"code": "INACCESSIBLE_USED_WITH_EXTERNAL",
"locations": Array [
Object {
"column": 25,
"line": 7,
},
],
"message": "[serviceA] Product.sku -> Found illegal use of @inaccessible directive. @inaccessible directives cannot currently be used in tandem with an @external directive.",
},
]
`);
});

it('errors when `@external` and `@inaccessible` are used on the same field of an interface extension', () => {
const serviceA = {
typeDefs: gql`
type Query {
product: Product @tag(name: "prod")
}
extend interface Product @key(fields: "sku") {
sku: String @external @inaccessible
}
`,
name: 'serviceA',
};

const errors = tagOrInaccessibleUsedWithExternal(serviceA);
expect(errors).toMatchInlineSnapshot(`
Array [
Object {
"code": "INACCESSIBLE_USED_WITH_EXTERNAL",
"locations": Array [
Object {
"column": 25,
"line": 7,
},
],
"message": "[serviceA] Product.sku -> Found illegal use of @inaccessible directive. @inaccessible directives cannot currently be used in tandem with an @external directive.",
},
]
`);
});
});
});

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ export { keyFieldsMissingExternal } from './keyFieldsMissingExternal';
export { reservedFieldUsed } from './reservedFieldUsed';
export { duplicateEnumOrScalar } from './duplicateEnumOrScalar';
export { duplicateEnumValue } from './duplicateEnumValue';
export { tagUsedWithExternal } from './tagUsedWithExternal';
export { tagOrInaccessibleUsedWithExternal } from './tagOrInaccessibleUsedWithExternal';
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import {
} from '../../utils';

/**
* There are no fields with both @tag and @external. We're only concerned with
* the xTypeExtension nodes because @external is not allowed on base types.
* Ensure there are no fields with @external and _either_ @tag or @inaccessible.
* We're only concerned with the xTypeExtension nodes because @external is
* already disallowed on base types.
*/
export const tagUsedWithExternal = ({
export const tagOrInaccessibleUsedWithExternal = ({
name: serviceName,
typeDefs,
}: ServiceDefinition) => {
Expand All @@ -28,14 +29,17 @@ export const tagUsedWithExternal = ({
});

function fieldsVisitor(
typeDefinition:
| ObjectTypeExtensionNode
| InterfaceTypeExtensionNode,
typeDefinition: ObjectTypeExtensionNode | InterfaceTypeExtensionNode,
) {
if (!typeDefinition.fields) return;
for (const fieldDefinition of typeDefinition.fields) {
const tagDirectives = findDirectivesOnNode(fieldDefinition, 'tag');
const hasTagDirective = tagDirectives.length > 0;
const inaccessibleDirectives = findDirectivesOnNode(
fieldDefinition,
'inaccessible',
);
const hasInaccessibleDirective = inaccessibleDirectives.length > 0;
const hasExternalDirective =
findDirectivesOnNode(fieldDefinition, 'external').length > 0;
if (hasTagDirective && hasExternalDirective) {
Expand All @@ -52,6 +56,21 @@ export const tagUsedWithExternal = ({
),
);
}

if (hasInaccessibleDirective && hasExternalDirective) {
errors.push(
errorWithCode(
'INACCESSIBLE_USED_WITH_EXTERNAL',
logServiceAndType(
serviceName,
typeDefinition.name.value,
fieldDefinition.name.value,
) +
`Found illegal use of @inaccessible directive. @inaccessible directives cannot currently be used in tandem with an @external directive.`,
inaccessibleDirectives,
),
);
}
}
}

Expand Down

0 comments on commit 3d51e58

Please sign in to comment.