Skip to content

Commit 8af9ed5

Browse files
authored
feat(utils): makeExtendSchemaPlugin interfaces support (#696)
1 parent c837e09 commit 8af9ed5

6 files changed

Lines changed: 430 additions & 8 deletions

File tree

packages/graphile-build/src/SchemaBuilder.d.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
GraphQLInputFieldConfigMap,
1515
GraphQLInputFieldConfig,
1616
GraphQLUnionTypeConfig,
17+
GraphQLInterfaceTypeConfig,
1718
} from "graphql";
1819
import { EventEmitter } from "events";
1920

@@ -182,6 +183,34 @@ export default class SchemaBuilder extends EventEmitter {
182183
before?: Array<string>,
183184
after?: Array<string>
184185
): void;
186+
hook<TSource, TContext>(
187+
hookName: "GraphQLInterfaceType",
188+
fn: Hook<GraphQLInterfaceTypeConfig<TSource, TContext>>,
189+
provides?: Array<string>,
190+
before?: Array<string>,
191+
after?: Array<string>
192+
): void;
193+
hook<TSource, TContext>(
194+
hookName: "GraphQLInterfaceType:fields",
195+
fn: Hook<GraphQLFieldConfigMap<TSource, TContext>>,
196+
provides?: Array<string>,
197+
before?: Array<string>,
198+
after?: Array<string>
199+
): void;
200+
hook<TSource, TContext>(
201+
hookName: "GraphQLInterfaceType:fields:field",
202+
fn: Hook<GraphQLFieldConfig<TSource, TContext>>,
203+
provides?: Array<string>,
204+
before?: Array<string>,
205+
after?: Array<string>
206+
): void;
207+
hook(
208+
hookName: "GraphQLInterfaceType:fields:field:args",
209+
fn: Hook<GraphQLFieldConfigArgumentMap>,
210+
provides?: Array<string>,
211+
before?: Array<string>,
212+
after?: Array<string>
213+
): void;
185214
hook(
186215
hookName: "finalize",
187216
fn: Hook<GraphQLSchema>,

packages/graphile-build/src/SchemaBuilder.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,19 @@ class SchemaBuilder extends EventEmitter {
231231
// - 'GraphQLUnionType:types' to add additional types to this union
232232
GraphQLUnionType: [],
233233
"GraphQLUnionType:types": [],
234+
235+
// When creating a GraphQLInterfaceType via `newWithHooks`, we'll
236+
// execute, the following hooks:
237+
// - 'GraphQLInterfaceType' to add any root-level attributes, e.g. add a description
238+
// - 'GraphQLInterfaceType:fields' to add additional fields to this interface type (is
239+
// ran asynchronously and gets a reference to the final GraphQL Interface as
240+
// `Self` in the context)
241+
// - 'GraphQLInterfaceType:fields:field' to customise an individual field from above
242+
// - 'GraphQLInterfaceType:fields:field:args' to customize the arguments to a field
243+
GraphQLInterfaceType: [],
244+
"GraphQLInterfaceType:fields": [],
245+
"GraphQLInterfaceType:fields:field": [],
246+
"GraphQLInterfaceType:fields:field:args": [],
234247
};
235248
}
236249

packages/graphile-build/src/makeNewBuild.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,126 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } {
860860
);
861861
},
862862
};
863+
} else if (Type === GraphQLInterfaceType) {
864+
const commonContext = {
865+
type: "GraphQLInterfaceType",
866+
scope,
867+
};
868+
newSpec = builder.applyHooks(
869+
this,
870+
"GraphQLInterfaceType",
871+
newSpec,
872+
commonContext,
873+
`|${newSpec.name}`
874+
);
875+
876+
const rawSpec = newSpec;
877+
newSpec = {
878+
...newSpec,
879+
fields: () => {
880+
const processedFields = [];
881+
const fieldsContext = {
882+
...commonContext,
883+
Self,
884+
GraphQLInterfaceType: rawSpec,
885+
fieldWithHooks: ((fieldName, spec, fieldScope) => {
886+
if (!isString(fieldName)) {
887+
throw new Error(
888+
"It looks like you forgot to pass the fieldName to `fieldWithHooks`, we're sorry this is currently necessary."
889+
);
890+
}
891+
if (!fieldScope) {
892+
throw new Error(
893+
"All calls to `fieldWithHooks` must specify a `fieldScope` " +
894+
"argument that gives additional context about the field so " +
895+
"that further plugins may more easily understand the field. " +
896+
"Keys within this object should contain the phrase 'field' " +
897+
"since they will be merged into the parent objects scope and " +
898+
"are not allowed to clash. If you really have no additional " +
899+
"information to give, please just pass `{}`."
900+
);
901+
}
902+
903+
let newSpec = spec;
904+
const context = {
905+
...commonContext,
906+
Self,
907+
scope: extend(
908+
extend(
909+
{ ...scope },
910+
{
911+
fieldName,
912+
},
913+
`Within context for GraphQLInterfaceType '${rawSpec.name}'`
914+
),
915+
fieldScope,
916+
`Extending scope for field '${fieldName}' within context for GraphQLInterfaceType '${rawSpec.name}'`
917+
),
918+
};
919+
if (typeof newSpec === "function") {
920+
newSpec = newSpec(context);
921+
}
922+
newSpec = builder.applyHooks(
923+
this,
924+
"GraphQLInterfaceType:fields:field",
925+
newSpec,
926+
context,
927+
`|${getNameFromType(Self)}.fields.${fieldName}`
928+
);
929+
newSpec.args = newSpec.args || {};
930+
newSpec = {
931+
...newSpec,
932+
args: builder.applyHooks(
933+
this,
934+
"GraphQLInterfaceType:fields:field:args",
935+
newSpec.args,
936+
{
937+
...context,
938+
field: newSpec,
939+
returnType: newSpec.type,
940+
},
941+
`|${getNameFromType(Self)}.fields.${fieldName}`
942+
),
943+
};
944+
const finalSpec = newSpec;
945+
processedFields.push(finalSpec);
946+
return finalSpec;
947+
}: FieldWithHooksFunction),
948+
};
949+
let rawFields = rawSpec.fields || {};
950+
if (typeof rawFields === "function") {
951+
rawFields = rawFields(fieldsContext);
952+
}
953+
const fieldsSpec = builder.applyHooks(
954+
this,
955+
"GraphQLInterfaceType:fields",
956+
this.extend(
957+
{},
958+
rawFields,
959+
`Default field included in newWithHooks call for '${
960+
rawSpec.name
961+
}'. ${inScope.__origin || ""}`
962+
),
963+
fieldsContext,
964+
`|${rawSpec.name}`
965+
);
966+
// Finally, check through all the fields that they've all been processed; any that have not we should do so now.
967+
for (const fieldName in fieldsSpec) {
968+
const fieldSpec = fieldsSpec[fieldName];
969+
if (processedFields.indexOf(fieldSpec) < 0) {
970+
// We've not processed this yet; process it now!
971+
fieldsSpec[fieldName] = fieldsContext.fieldWithHooks(
972+
fieldName,
973+
fieldSpec,
974+
{
975+
autoField: true, // We don't have any additional information
976+
}
977+
);
978+
}
979+
}
980+
return fieldsSpec;
981+
},
982+
};
863983
}
864984

865985
const finalSpec: ConfigType = newSpec;

packages/graphile-utils/__tests__/ExtendSchemaPlugin.test.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,3 +795,109 @@ it("supports defining a simple subscription", async () => {
795795
expect(done).toBeTruthy();
796796
expect(value).toBe(undefined);
797797
});
798+
799+
it("supports interfaces and unions", async () => {
800+
const schema = await buildSchema([
801+
...simplePlugins,
802+
makeExtendSchemaPlugin(_build => ({
803+
typeDefs: gql`
804+
interface Named {
805+
name: String!
806+
}
807+
808+
type A implements Named {
809+
name: String!
810+
a: Int!
811+
}
812+
type B implements Named {
813+
name(suffix: String): String!
814+
b: Int!
815+
}
816+
type C {
817+
c: Int!
818+
}
819+
820+
union ABC = A | B | C
821+
extend type Query {
822+
abc: [ABC!]
823+
named: [Named!]
824+
}
825+
826+
extend interface Named {
827+
nameLanguage: String
828+
}
829+
extend type A {
830+
nameLanguage: String
831+
}
832+
extend type B {
833+
nameLanguage: String
834+
}
835+
`,
836+
resolvers: {
837+
Query: {
838+
abc: () => [
839+
{ name: "A-one", a: 1, nameLanguage: "en" },
840+
{ name: "B-two", b: 2, nameLanguage: "en" },
841+
{ c: 3 },
842+
],
843+
named: () => [
844+
{ name: "A-one", a: 1, nameLanguage: "en" },
845+
{ name: "B-two", b: 2, nameLanguage: "en" },
846+
],
847+
},
848+
ABC: {
849+
__resolveType(obj) {
850+
if (obj.a != null) return "A";
851+
if (obj.b != null) return "B";
852+
if (obj.c != null) return "C";
853+
return null;
854+
},
855+
},
856+
Named: {
857+
__resolveType(obj) {
858+
if (obj.a != null) return "A";
859+
if (obj.b != null) return "B";
860+
return null;
861+
},
862+
},
863+
},
864+
})),
865+
]);
866+
expect(schema).toMatchSnapshot();
867+
const { data, errors } = await graphql(
868+
schema,
869+
`
870+
query {
871+
abc {
872+
__typename
873+
... on A {
874+
name
875+
nameLanguage
876+
a
877+
}
878+
... on B {
879+
name
880+
nameLanguage
881+
b
882+
}
883+
... on C {
884+
c
885+
}
886+
}
887+
named {
888+
__typename
889+
name
890+
nameLanguage
891+
... on A {
892+
a
893+
}
894+
... on B {
895+
b
896+
}
897+
}
898+
}
899+
`
900+
);
901+
expect(errors).toBeFalsy();
902+
expect(data).toMatchSnapshot();
903+
});

packages/graphile-utils/__tests__/__snapshots__/ExtendSchemaPlugin.test.js.snap

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,3 +393,77 @@ Object {
393393
},
394394
}
395395
`;
396+
397+
exports[`supports interfaces and unions 1`] = `
398+
"""The root query type which gives access points into the data universe."""
399+
type Query {
400+
"""
401+
Exposes the root query type nested one level down. This is helpful for Relay 1
402+
which can only query top level fields if they are in a particular form.
403+
"""
404+
query: Query!
405+
abc: [ABC!]
406+
named: [Named!]
407+
}
408+
409+
union ABC = A | B | C
410+
411+
type A implements Named {
412+
name: String!
413+
a: Int!
414+
nameLanguage: String
415+
}
416+
417+
interface Named {
418+
name: String!
419+
nameLanguage: String
420+
}
421+
422+
type B implements Named {
423+
name(suffix: String): String!
424+
b: Int!
425+
nameLanguage: String
426+
}
427+
428+
type C {
429+
c: Int!
430+
}
431+
432+
`;
433+
434+
exports[`supports interfaces and unions 2`] = `
435+
Object {
436+
"abc": Array [
437+
Object {
438+
"__typename": "A",
439+
"a": 1,
440+
"name": "A-one",
441+
"nameLanguage": "en",
442+
},
443+
Object {
444+
"__typename": "B",
445+
"b": 2,
446+
"name": "B-two",
447+
"nameLanguage": "en",
448+
},
449+
Object {
450+
"__typename": "C",
451+
"c": 3,
452+
},
453+
],
454+
"named": Array [
455+
Object {
456+
"__typename": "A",
457+
"a": 1,
458+
"name": "A-one",
459+
"nameLanguage": "en",
460+
},
461+
Object {
462+
"__typename": "B",
463+
"b": 2,
464+
"name": "B-two",
465+
"nameLanguage": "en",
466+
},
467+
],
468+
}
469+
`;

0 commit comments

Comments
 (0)