graphql: validate well-foundedness of unions (#835)

Summary:
GraphQL unions are required to be unions specifically of object types.
They cannot contain primitives or other union types as clauses. This is
good: it means that we don’t have to worry about unions that recursively
reference each other or themselves.

Unions are also required to have at least one clause, but we don’t
validate this because it’s not helpful for us. An empty union is
perfectly well-defined, if useless, and shouldn’t cause any problems.

Relevant portion of the spec:
<https://facebook.github.io/graphql/October2016/#sec-Union-type-validation>

Test Plan:
Unit tests added, retaining full coverage; `yarn unit` suffices.

wchargin-branch: graphql-schema-union-validation
This commit is contained in:
William Chargin 2018-09-13 18:11:26 -07:00 committed by GitHub
parent 7da9ef3a94
commit 4675b84443
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 89 additions and 11 deletions

View File

@ -46,7 +46,39 @@ export type FieldType =
const ID_FIELD_NAME = "id";
export function schema(types: {[Typename]: NodeType}): Schema {
return {...types};
const result = {};
for (const typename of Object.keys(types)) {
const type = types[typename];
switch (type.type) {
case "OBJECT":
result[typename] = {type: "OBJECT", fields: {...type.fields}};
break;
case "UNION":
for (const clause of Object.keys(type.clauses)) {
const clauseType = types[clause];
if (clauseType == null) {
throw new Error(
`union has unknown clause: "${typename}"/"${clause}"`
);
}
if (clauseType.type !== "OBJECT") {
// The GraphQL spec doesn't permit unions of interfaces or
// other unions (or primitives). This is nice, because it
// means that we don't have to worry about ill-founded
// unions.
throw new Error(
`union has non-object type clause: "${typename}"/"${clause}"`
);
}
}
result[typename] = {type: "UNION", clauses: {...type.clauses}};
break;
// istanbul ignore next
default:
throw new Error((type.type: empty));
}
}
return result;
}
export function object(fields: {[Fieldname]: FieldType}): NodeType {

View File

@ -3,9 +3,9 @@
import * as Schema from "./schema";
describe("graphql/schema", () => {
function buildGithubSchema(): Schema.Schema {
function buildGithubTypes(): {[Schema.Typename]: Schema.NodeType} {
const s = Schema;
return s.schema({
return {
Repository: s.object({
id: s.id(),
url: s.primitive(),
@ -40,16 +40,62 @@ describe("graphql/schema", () => {
url: s.primitive(),
login: s.primitive(),
}),
});
};
}
function buildGithubSchema(): Schema.Schema {
return Schema.schema(buildGithubTypes());
}
it("builds a representative schema", () => {
const githubSchema = buildGithubSchema();
expect(typeof githubSchema).toBe("object");
});
it("passes through serialization unscathed", () => {
const schema = buildGithubSchema();
expect(JSON.parse(JSON.stringify(schema))).toEqual(schema);
describe("schema", () => {
it("builds a representative schema", () => {
const githubSchema = buildGithubSchema();
expect(typeof githubSchema).toBe("object");
});
it("is deep-equal to but deep-distinct from its input", () => {
const githubTypes = buildGithubTypes();
const schema = Schema.schema(githubTypes);
expect(githubTypes).toEqual(schema);
expect(githubTypes).not.toBe(schema);
expect(githubTypes.Repository).not.toBe(schema.Repository);
{
const a = githubTypes.Repository;
const b = schema.Repository;
if (a.type !== "OBJECT") throw new Error("githubTypes: " + a.type);
if (b.type !== "OBJECT") throw new Error("schema: " + b.type);
expect(a.fields).not.toBe(b.fields);
}
{
const a = githubTypes.Actor;
const b = schema.Actor;
if (a.type !== "UNION") throw new Error("githubTypes: " + a.type);
if (b.type !== "UNION") throw new Error("schema: " + b.type);
expect(a.clauses).not.toBe(b.clauses);
}
});
it("passes through serialization unscathed", () => {
const schema = buildGithubSchema();
expect(JSON.parse(JSON.stringify(schema))).toEqual(schema);
});
it("disallows unions with unknown clauses", () => {
const s = Schema;
const types = {
U: s.union(["Wat"]),
};
expect(() => Schema.schema(types)).toThrowError(
'union has unknown clause: "U"/"Wat"'
);
});
it("disallows unions with non-object clauses", () => {
const s = Schema;
const types = {
O: s.object({id: s.id()}),
U1: s.union(["O"]),
U2: s.union(["U1"]),
};
expect(() => Schema.schema(types)).toThrowError(
'union has non-object type clause: "U2"/"U1"'
);
});
});
describe("object", () => {