schema: support shallow-nested object fields (#916)

Summary:
See #915 for context. This commit changes the `schema` module only.

I had a hard time picking names that clearly distinguish the top-level
field on the object and the subfields that it contains. @decentralion
and I independently came up with “nest” and “egg”. It’s a bit colorful,
but it’s certainly easy to remember which one is which, and it doesn’t
conflict with existing notions like “parent”/“child”.

Test Plan:
Unit tests expanded slightly, retaining full coverage.

wchargin-branch: schema-shallow
This commit is contained in:
William Chargin 2018-10-04 15:28:58 -07:00 committed by GitHub
parent d8d857fdd3
commit 3d2206088c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 52 additions and 8 deletions

View File

@ -1056,6 +1056,8 @@ export class Mirror {
case "CONNECTION": case "CONNECTION":
// Not handled by this function. // Not handled by this function.
return null; return null;
case "NESTED":
throw new Error("Nested fields not supported.");
// istanbul ignore next // istanbul ignore next
default: default:
throw new Error((field.type: empty)); throw new Error((field.type: empty));
@ -1563,6 +1565,8 @@ export function _buildSchemaInfo(schema: Schema.Schema): SchemaInfo {
case "CONNECTION": case "CONNECTION":
entry.connectionFieldNames.push(fieldname); entry.connectionFieldNames.push(fieldname);
break; break;
case "NESTED":
throw new Error("Nested fields not supported.");
// istanbul ignore next // istanbul ignore next
default: default:
throw new Error((field.type: empty)); throw new Error((field.type: empty));

View File

@ -31,15 +31,41 @@ export type ObjectId = string;
// represented as `PRIMITIVE`s (except for `ID`s). // represented as `PRIMITIVE`s (except for `ID`s).
// - Connections are supported as object fields, but arbitrary lists // - Connections are supported as object fields, but arbitrary lists
// are not. // are not.
//
// To accommodate schemata where some object types do not have IDs,
// objects may have "nested" fields of primitive or node-reference type.
// These may be nested to depth exactly 1. Suppose that `Foo` is an
// object type that includes `bar: Bar!`, but `Bar` is an object type
// without an `id`. Then `Bar` may not be a first-class type, but `Foo`
// may pull properties off of it using
//
// bar: nested({x: primitive(), y: node("Baz")});
//
// The property "bar" in the above example is called a _nested_
// property, and its fields "x" and "y" are called _eggs_. (The nest
// contains the eggs.)
export type Schema = {+[Typename]: NodeType}; export type Schema = {+[Typename]: NodeType};
export type NodeType = export type NodeType =
| {|+type: "OBJECT", +fields: {|+[Fieldname]: FieldType|}|} | {|+type: "OBJECT", +fields: {|+[Fieldname]: FieldType|}|}
| {|+type: "UNION", +clauses: {|+[Typename]: true|}|}; | {|+type: "UNION", +clauses: {|+[Typename]: true|}|};
export type FieldType = export type FieldType =
| {|+type: "ID"|} | IdFieldType
| {|+type: "PRIMITIVE"|} | PrimitiveFieldType
| {|+type: "NODE", +elementType: Typename|} | NodeFieldType
| {|+type: "CONNECTION", +elementType: Typename|}; | ConnectionFieldType
| NestedFieldType;
export type IdFieldType = {|+type: "ID"|};
export type PrimitiveFieldType = {|+type: "PRIMITIVE"|};
export type NodeFieldType = {|+type: "NODE", +elementType: Typename|};
export type ConnectionFieldType = {|
+type: "CONNECTION",
+elementType: Typename,
|};
export type NestedFieldType = {|
+type: "NESTED",
+eggs: {+[Fieldname]: PrimitiveFieldType | NodeFieldType},
|};
// Every object must have exactly one `id` field, and it must have this // Every object must have exactly one `id` field, and it must have this
// name. // name.
@ -111,18 +137,24 @@ export function union(clauses: $ReadOnlyArray<Typename>): NodeType {
return {type: "UNION", clauses: clausesMap}; return {type: "UNION", clauses: clausesMap};
} }
export function id(): FieldType { export function id(): IdFieldType {
return {type: "ID"}; return {type: "ID"};
} }
export function primitive(): FieldType { export function primitive(): PrimitiveFieldType {
return {type: "PRIMITIVE"}; return {type: "PRIMITIVE"};
} }
export function node(elementType: Typename): FieldType { export function node(elementType: Typename): NodeFieldType {
return {type: "NODE", elementType}; return {type: "NODE", elementType};
} }
export function connection(elementType: Typename): FieldType { export function connection(elementType: Typename): ConnectionFieldType {
return {type: "CONNECTION", elementType}; return {type: "CONNECTION", elementType};
} }
export function nested(eggs: {
+[Fieldname]: PrimitiveFieldType | NodeFieldType,
}): NestedFieldType {
return {type: "NESTED", eggs: {...eggs}};
}

View File

@ -19,6 +19,14 @@ describe("graphql/schema", () => {
title: s.primitive(), title: s.primitive(),
comments: s.connection("IssueComment"), comments: s.connection("IssueComment"),
}), }),
Commit: s.object({
id: s.id(),
oid: s.primitive(),
author: /* GitActor */ s.nested({
date: s.primitive(),
user: s.node("User"),
}),
}),
IssueComment: s.object({ IssueComment: s.object({
id: s.id(), id: s.id(),
body: s.primitive(), body: s.primitive(),