Skip to content

Commit

Permalink
Demand control directive validations (#3148)
Browse files Browse the repository at this point in the history
Adds validations to subgraph directive applications, per the
specification in
https://ibm.github.io/graphql-specs/cost-spec.html#sec-Validation. These
rules primarily assert that the @cost and @listsize directive arguments
reference valid fields of the correct type for each application.

<!-- ROUTER-741 -->
  • Loading branch information
tninesling authored Sep 24, 2024
1 parent 4b3bcbb commit 5ac01b5
Show file tree
Hide file tree
Showing 5 changed files with 359 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/tidy-trees-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/federation-internals": patch
---

Add validations for demand control directive applications
2 changes: 1 addition & 1 deletion docs/source/federation-versions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ First release

Minimum router version

**TBD**
**1.53.0**

</div>

Expand Down
185 changes: 185 additions & 0 deletions internals-js/src/__tests__/subgraphValidation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1484,3 +1484,188 @@ describe('@interfaceObject/@key on interfaces validation', () => {
]);
});
});

describe('@cost', () => {
it('rejects applications on interfaces', () => {
const doc = gql`
extend schema
@link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@cost"])
type Query {
a: A
}
interface A {
x: Int @cost(weight: 10)
}
`;

expect(buildForErrors(doc)).toStrictEqual([
[
'COST_APPLIED_TO_INTERFACE_FIELD',
`[S] @cost cannot be applied to interface "A.x"`,
],
]);
});
});

describe('@listSize', () => {
it('rejects applications on non-lists (unless it uses sizedFields)', () => {
const doc = gql`
extend schema
@link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"])
type Query {
a1: A @listSize(assumedSize: 5)
a2: A @listSize(assumedSize: 10, sizedFields: ["ints"])
}
type A {
ints: [Int]
}
`;

expect(buildForErrors(doc)).toStrictEqual([
['LIST_SIZE_APPLIED_TO_NON_LIST', `[S] "Query.a1" is not a list`],
]);
});

it('rejects negative assumedSize', () => {
const doc = gql`
extend schema
@link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"])
type Query {
a: [Int] @listSize(assumedSize: -5)
b: [Int] @listSize(assumedSize: 0)
}
`;

expect(buildForErrors(doc)).toStrictEqual([
[
'LIST_SIZE_INVALID_ASSUMED_SIZE',
`[S] Assumed size of "Query.a" cannot be negative`,
],
]);
});

it('rejects slicingArguments which are not arguments of the field', () => {
const doc = gql`
extend schema
@link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"])
type Query {
myField(something: Int): [String]
@listSize(slicingArguments: ["missing1", "missing2"])
myOtherField(somethingElse: String): [Int]
@listSize(slicingArguments: ["alsoMissing"])
}
`;

expect(buildForErrors(doc)).toStrictEqual([
[
'LIST_SIZE_INVALID_SLICING_ARGUMENT',
`[S] Slicing argument "missing1" is not an argument of "Query.myField"`,
],
[
'LIST_SIZE_INVALID_SLICING_ARGUMENT',
`[S] Slicing argument "missing2" is not an argument of "Query.myField"`,
],
[
'LIST_SIZE_INVALID_SLICING_ARGUMENT',
`[S] Slicing argument "alsoMissing" is not an argument of "Query.myOtherField"`,
],
]);
});

it('rejects slicingArguments which are not Int or Int!', () => {
const doc = gql`
extend schema
@link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"])
type Query {
sliced(first: String, second: Int, third: Int!): [String]
@listSize(slicingArguments: ["first", "second", "third"])
}
`;

expect(buildForErrors(doc)).toStrictEqual([
[
'LIST_SIZE_INVALID_SLICING_ARGUMENT',
`[S] Slicing argument "Query.sliced(first:)" must be Int or Int!`,
],
]);
});

it('rejects sizedFields when the output type is not an object', () => {
const doc = gql`
extend schema
@link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"])
type Query {
notObject: Int @listSize(assumedSize: 1, sizedFields: ["anything"])
a: A @listSize(assumedSize: 5, sizedFields: ["ints"])
b: B @listSize(assumedSize: 10, sizedFields: ["ints"])
}
type A {
ints: [Int]
}
interface B {
ints: [Int]
}
`;

expect(buildForErrors(doc)).toStrictEqual([
[
'LIST_SIZE_INVALID_SIZED_FIELD',
`[S] Sized fields cannot be used because "Int" is not an object type`,
],
]);
});

it('rejects sizedFields which are not fields of the output type', () => {
const doc = gql`
extend schema
@link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"])
type Query {
a: A @listSize(assumedSize: 5, sizedFields: ["notOnA"])
}
type A {
ints: [Int]
}
`;

expect(buildForErrors(doc)).toStrictEqual([
[
'LIST_SIZE_INVALID_SIZED_FIELD',
`[S] Sized field "notOnA" is not a field on type "A"`,
],
]);
});

it('rejects sizedFields which are not lists', () => {
const doc = gql`
extend schema
@link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"])
type Query {
a: A @listSize(assumedSize: 5, sizedFields: ["notList"])
}
type A {
notList: String
}
`;

expect(buildForErrors(doc)).toStrictEqual([
[
'LIST_SIZE_APPLIED_TO_NON_LIST',
`[S] Sized field "A.notList" is not a list`,
],
]);
});
});
36 changes: 36 additions & 0 deletions internals-js/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,36 @@ const CONTEXTUAL_ARGUMENT_NOT_CONTEXTUAL_IN_ALL_SUBGRAPHS = makeCodeDefinition(
{ addedIn: '2.7.0' },
);

const COST_APPLIED_TO_INTERFACE_FIELD = makeCodeDefinition(
'COST_APPLIED_TO_INTERFACE_FIELD',
'The `@cost` directive must be applied to concrete types',
{ addedIn: '2.9.2' },
);

const LIST_SIZE_APPLIED_TO_NON_LIST = makeCodeDefinition(
'LIST_SIZE_APPLIED_TO_NON_LIST',
'The `@listSize` directive must be applied to list types',
{ addedIn: '2.9.2' },
);

const LIST_SIZE_INVALID_ASSUMED_SIZE = makeCodeDefinition(
'LIST_SIZE_INVALID_ASSUMED_SIZE',
'The `@listSize` directive assumed size cannot be negative',
{ addedIn: '2.9.2' },
);

const LIST_SIZE_INVALID_SLICING_ARGUMENT = makeCodeDefinition(
'LIST_SIZE_INVALID_SLICING_ARGUMENT',
'The `@listSize` directive must have existing integer slicing arguments',
{ addedIn: '2.9.2' },
);

const LIST_SIZE_INVALID_SIZED_FIELD = makeCodeDefinition(
'LIST_SIZE_INVALID_SIZED_FIELD',
'The `@listSize` directive must reference existing list fields as sized fields',
{ addedIn: '2.9.2' },
);

export const ERROR_CATEGORIES = {
DIRECTIVE_FIELDS_MISSING_EXTERNAL,
DIRECTIVE_UNSUPPORTED_ON_INTERFACE,
Expand Down Expand Up @@ -824,6 +854,12 @@ export const ERRORS = {
SOURCE_FIELD_SELECTION_INVALID,
SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD,
CONTEXTUAL_ARGUMENT_NOT_CONTEXTUAL_IN_ALL_SUBGRAPHS,
// Errors related to demand control
COST_APPLIED_TO_INTERFACE_FIELD,
LIST_SIZE_APPLIED_TO_NON_LIST,
LIST_SIZE_INVALID_ASSUMED_SIZE,
LIST_SIZE_INVALID_SIZED_FIELD,
LIST_SIZE_INVALID_SLICING_ARGUMENT,
};

const codeDefByCode = Object.values(ERRORS).reduce((obj: {[code: string]: ErrorCodeDefinition}, codeDef: ErrorCodeDefinition) => { obj[codeDef.code] = codeDef; return obj; }, {});
Expand Down
Loading

0 comments on commit 5ac01b5

Please sign in to comment.