Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] CCN: Validation #3665

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,137 @@ describe('Validate: Overlapping fields can be merged', () => {
);
});

it('allows non-conflicting overlapping required statuses', () => {
expectValidationErrorsWithSchema(
schema,
OverlappingFieldsCanBeMergedRule,
`
{
someBox {
... on IntBox {
scalar: unrelatedField!
}
... on StringBox {
scalar!
}
}
}
`,
{
experimentalClientControlledNullability: true,
},
);

expectValidationErrorsWithSchema(
schema,
OverlappingFieldsCanBeMergedRule,
`
{
someBox {
... on IntBox {
scalar: unrelatedField?
}
... on StringBox {
scalar?
}
}
}
`,
{
experimentalClientControlledNullability: true,
},
);
});

it('disallows conflicting overlapping required statuses', () => {
expectValidationErrorsWithSchema(
schema,
OverlappingFieldsCanBeMergedRule,
`
{
someBox {
... on IntBox {
scalar: unrelatedField!
}
... on StringBox {
scalar
}
}
}
`,
{
experimentalClientControlledNullability: true,
},
).toDeepEqual([
{
message:
'Fields "scalar" conflict because they have conflicting nullability designators "!" and "". Use different aliases on the fields to fetch both if this was intentional.',
locations: [
{ line: 5, column: 17 },
{ line: 8, column: 17 },
],
},
]);

expectValidationErrorsWithSchema(
schema,
OverlappingFieldsCanBeMergedRule,
`
{
someBox {
... on IntBox {
scalar: unrelatedField!
}
... on StringBox {
scalar?
}
}
}
`,
{
experimentalClientControlledNullability: true,
},
).toDeepEqual([
{
message:
'Fields "scalar" conflict because they have conflicting nullability designators "!" and "?". Use different aliases on the fields to fetch both if this was intentional.',
locations: [
{ line: 5, column: 17 },
{ line: 8, column: 17 },
],
},
]);

expectValidationErrorsWithSchema(
schema,
OverlappingFieldsCanBeMergedRule,
`
{
someBox {
... on IntBox {
scalar: unrelatedField
}
... on StringBox {
scalar?
}
}
}
`,
{
experimentalClientControlledNullability: true,
},
).toDeepEqual([
{
message:
'Fields "scalar" conflict because they have conflicting nullability designators "" and "?". Use different aliases on the fields to fetch both if this was intentional.',
locations: [
{ line: 5, column: 17 },
{ line: 8, column: 17 },
],
},
]);
});

it('same wrapped scalar return types', () => {
expectValidWithSchema(
schema,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, it } from 'mocha';

import { buildSchema } from '../../utilities/buildASTSchema.js';

import { RequiredStatusOnFieldMatchesDefinitionRule } from '../rules/RequiredStatusOnFieldMatchesDefinitionRule.js';

import { expectValidationErrorsWithSchema } from './harness.js';

function expectErrors(queryStr: string) {
return expectValidationErrorsWithSchema(
testSchema,
RequiredStatusOnFieldMatchesDefinitionRule,
queryStr,
{ experimentalClientControlledNullability: true },
);
}

function expectValid(queryStr: string) {
expectErrors(queryStr).toDeepEqual([]);
}

const testSchema = buildSchema(`
type Lists {
nonList: Int
list: [Int]
requiredList: [Int]!
mixedThreeDList: [[[Int]]]
}
type Query {
lists: Lists
}
`);

describe('Validate: Field uses correct list depth', () => {
it('Fields are valid', () => {
expectValid(`
fragment listFragment on Lists {
list[!]
nonList!
nonList?
mixedThreeDList[[[!]!]!]!
requiredList[]
unmodifiedList: list
}
`);
});

it('reports errors when list depth is too high', () => {
expectErrors(`
fragment listFragment on Lists {
notAList: nonList[!]
list[[]]
}
`).toDeepEqual([
{
message: 'List nullability modifier is too deep.',
locations: [{ line: 3, column: 26 }],
},
{
message: 'List nullability modifier is too deep.',
locations: [{ line: 4, column: 13 }],
},
]);
});

it('reports errors when list depth is too low', () => {
expectErrors(`
fragment listFragment on Lists {
list!
mixedThreeDList[[]!]!
}
`).toDeepEqual([
{
message: 'List nullability modifier is too shallow.',
locations: [{ line: 3, column: 13 }],
},
{
message: 'List nullability modifier is too shallow.',
locations: [{ line: 4, column: 24 }],
},
]);
});
});
12 changes: 10 additions & 2 deletions src/validation/__tests__/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expectJSON } from '../../__testUtils__/expectJSON.js';

import type { Maybe } from '../../jsutils/Maybe.js';

import type { ParseOptions } from '../../language/parser.js';
import { parse } from '../../language/parser.js';

import type { GraphQLSchema } from '../../type/schema.js';
Expand Down Expand Up @@ -122,17 +123,24 @@ export function expectValidationErrorsWithSchema(
schema: GraphQLSchema,
rule: ValidationRule,
queryStr: string,
parseOptions?: ParseOptions,
): any {
const doc = parse(queryStr);
const doc = parse(queryStr, parseOptions);
const errors = validate(schema, doc, [rule]);
return expectJSON(errors);
}

export function expectValidationErrors(
rule: ValidationRule,
queryStr: string,
parseOptions?: ParseOptions,
): any {
return expectValidationErrorsWithSchema(testSchema, rule, queryStr);
return expectValidationErrorsWithSchema(
testSchema,
rule,
queryStr,
parseOptions,
);
}

export function expectSDLValidationErrors(
Expand Down
45 changes: 34 additions & 11 deletions src/validation/rules/OverlappingFieldsCanBeMergedRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -617,17 +617,40 @@ function findConflict(
const type1 = def1?.type;
const type2 = def2?.type;

if (type1 && type2 && doTypesConflict(type1, type2)) {
return [
[
responseName,
`they return conflicting types "${inspect(type1)}" and "${inspect(
type2,
)}"`,
],
[node1],
[node2],
];
if (type1 && type2) {
// Two fields have different types
if (doTypesConflict(type1, type2)) {
return [
[
responseName,
`they return conflicting types "${inspect(type1)}" and "${inspect(
type2,
)}"`,
],
[node1],
[node2],
];
}

// Two fields have different required operators
if (node1.nullabilityAssertion !== node2.nullabilityAssertion) {
return [
[
responseName,
`they have conflicting nullability designators "${
node1.nullabilityAssertion === undefined
? ''
: print(node1.nullabilityAssertion)
}" and "${
node2.nullabilityAssertion === undefined
? ''
: print(node2.nullabilityAssertion)
}"`,
],
[node1],
[node2],
];
}
}

// Collect and compare sub-fields. Use the same "visited fragment names" list
Expand Down
Loading