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

Add makeFederationSchemaPlugin. #19

Closed
wants to merge 1 commit into from
Closed
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
306 changes: 170 additions & 136 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,152 +1,203 @@
import {
AugmentedGraphQLFieldResolver,
ObjectFieldResolver,
makeExtendSchemaPlugin,
makePluginByCombiningPlugins,
gql,
} from "graphile-utils";
import { Plugin } from "graphile-build";
import { Build, Plugin } from "graphile-build";
import { DirectiveNode } from "graphql";
import printFederatedSchema from "./printFederatedSchema";
import { ObjectTypeDefinition, Directive, StringValue } from "./AST";

/**
* This plugin installs the schema outlined in the Apollo Federation spec, and
* the resolvers and types required. Comments have been added to make things
* clearer for consumers, and the Apollo fields have been deprecated so that
* users unconcerned with federation don't get confused.
*
* https://www.apollographql.com/docs/apollo-server/federation/federation-spec/#federation-schema-specification
* This function allows users to create their own version of the schema plugin.
*/
const SchemaExtensionPlugin = makeExtendSchemaPlugin(build => {
const {
graphql: { GraphQLScalarType, getNullableType },
resolveNode,
$$isQuery,
$$nodeType,
getTypeByName,
inflection,
nodeIdFieldName,
} = build;
// Cache
let Query: any;
return {
typeDefs: gql`
"""
Used to represent a federated entity via its keys.
"""
scalar _Any

"""
Used to represent a set of fields. Grammatically, a field set is a
selection set minus the braces.
"""
scalar _FieldSet

"""
A union of all federated types (those that use the @key directive).
"""
union _Entity

"""
Describes our federated service.
"""
type _Service {
export function makeFederationSchemaPlugin<TSource = any, TContext = any>(
generator: (
build: Build
) =>
| AugmentedGraphQLFieldResolver<TSource, TContext>
| ObjectFieldResolver<TSource, TContext>
): Plugin {
/**
* This plugin installs the schema outlined in the Apollo Federation spec, and
* the resolvers and types required. Comments have been added to make things
* clearer for consumers, and the Apollo fields have been deprecated so that
* users unconcerned with federation don't get confused.
*
* https://www.apollographql.com/docs/apollo-server/federation/federation-spec/#federation-schema-specification
*/
const SchemaExtensionPlugin = makeExtendSchemaPlugin(build => {
const {
graphql: { GraphQLScalarType, getNullableType },
$$isQuery,
$$nodeType,
getTypeByName,
inflection,
} = build;
// Cache
let Query: any;
return {
typeDefs: gql`
"""
The GraphQL Schema Language definiton of our endpoint including the
Apollo Federation directives (but not their definitions or the special
Apollo Federation fields).
Used to represent a federated entity via its keys.
"""
sdl: String
@deprecated(reason: "Only Apollo Federation should use this")
}
scalar _Any

extend type Query {
"""
Fetches a list of entities using their representations; used for Apollo
Federation.
Used to represent a set of fields. Grammatically, a field set is a
selection set minus the braces.
"""
_entities(representations: [_Any!]!): [_Entity]!
@deprecated(reason: "Only Apollo Federation should use this")
scalar _FieldSet

"""
Entrypoint for Apollo Federation to determine more information about
this service.
A union of all federated types (those that use the @key directive).
"""
_service: _Service!
@deprecated(reason: "Only Apollo Federation should use this")
}
union _Entity

directive @external on FIELD_DEFINITION
directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
`,
resolvers: {
Query: {
_entities(data, { representations }, context, resolveInfo) {
const {
graphile: { fieldContext },
} = resolveInfo;
return representations.map((representation: any) => {
if (!representation || typeof representation !== "object") {
throw new Error("Invalid representation");
}
const { __typename, [nodeIdFieldName]: nodeId } = representation;
if (!__typename || typeof nodeId !== "string") {
throw new Error("Failed to interpret representation");
}
return resolveNode(
nodeId,
build,
fieldContext,
data,
context,
resolveInfo
);
});
"""
Describes our federated service.
"""
type _Service {
"""
The GraphQL Schema Language definiton of our endpoint including the
Apollo Federation directives (but not their definitions or the special
Apollo Federation fields).
"""
sdl: String
@deprecated(reason: "Only Apollo Federation should use this")
}

extend type Query {
"""
Fetches a list of entities using their representations; used for Apollo
Federation.
"""
_entities(representations: [_Any!]!): [_Entity]!
@deprecated(reason: "Only Apollo Federation should use this")
"""
Entrypoint for Apollo Federation to determine more information about
this service.
"""
_service: _Service!
@deprecated(reason: "Only Apollo Federation should use this")
}

directive @external on FIELD_DEFINITION
directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
`,
resolvers: {
Query: {
_entities: generator(build),
_service(_, _args, _context, { schema }) {
return schema;
},
},

_service(_, _args, _context, { schema }) {
return schema;
_Service: {
sdl(schema) {
return printFederatedSchema(schema);
},
},
},

_Service: {
sdl(schema) {
return printFederatedSchema(schema);
_Entity: {
__resolveType(value) {
// This uses the same resolution as the Node interface, which can be found in graphile-build's NodePlugin
if (value === $$isQuery) {
if (!Query) Query = getTypeByName(inflection.builtin("Query"));
return Query;
} else if (value[$$nodeType]) {
return getNullableType(value[$$nodeType]);
}
},
},
},

_Entity: {
__resolveType(value) {
// This uses the same resolution as the Node interface, which can be found in graphile-build's NodePlugin
if (value === $$isQuery) {
if (!Query) Query = getTypeByName(inflection.builtin("Query"));
return Query;
} else if (value[$$nodeType]) {
return getNullableType(value[$$nodeType]);
}
},
_Any: new GraphQLScalarType({
name: "_Any",
serialize(value: any) {
return value;
},
}),
},
};
});

_Any: new GraphQLScalarType({
name: "_Any",
serialize(value: any) {
return value;
},
}),
},
/*
* This plugin adds types to the _Entity union defined above.
*/
const EntityPlugin: Plugin = builder => {
// Add our collected types to the _Entity union
builder.hook("GraphQLUnionType:types", (types, build, context) => {
const { Self } = context;
// If it's not the _Entity union, don't change it.
if (Self.name !== "_Entity") {
return types;
}

// If it's not the _Entity union, don't change it.
if (Self.name !== "_Entity") {
return types;
}
const { scopeByType } = build;

const newTypes = [...types];
for (const [type] of scopeByType) {
if (
type.astNode &&
type.astNode.directives &&
type.astNode.directives.some(
(directive: DirectiveNode) => directive.name.value === "key"
)
) {
newTypes.push(type);
}
}

// Add our types to the entity types
return newTypes;
});
};

return makePluginByCombiningPlugins(SchemaExtensionPlugin, EntityPlugin);
}

/**
* This plugin adds the federation-specific schema parts for resolving entities by nodeId.
*/
const FederationSchemaNodeIdPlugin = makeFederationSchemaPlugin(build => {
const { resolveNode, nodeIdFieldName } = build;
return (data, { representations }, context, resolveInfo) => {
const {
graphile: { fieldContext },
} = resolveInfo;
return representations.map((representation: any) => {
if (!representation || typeof representation !== "object") {
throw new Error("Invalid representation");
}
const { __typename, [nodeIdFieldName]: nodeId } = representation;
if (!__typename || typeof nodeId !== "string") {
throw new Error("Failed to interpret representation");
}
return resolveNode(
nodeId,
build,
fieldContext,
data,
context,
resolveInfo
);
});
};
});

/*
* This plugin adds the `@key(fields: "nodeId")` directive to the types that
* implement the Node interface, and adds these types to the _Entity union
* defined above.
* implement the Node interface and marks them to be added to the _Entity union.
*/
const AddKeyPlugin: Plugin = builder => {
builder.hook("build", build => {
build.federationEntityTypes = [];
return build;
});

const AddKeyNodeIdPlugin: Plugin = builder => {
// Find out what types implement the Node interface
builder.hook("GraphQLObjectType:interfaces", (interfaces, build, context) => {
const { getTypeByName, inflection, nodeIdFieldName } = build;
Expand All @@ -165,9 +216,6 @@ const AddKeyPlugin: Plugin = builder => {
return interfaces;
}

// Add this to the list of types to be in the _Entity union
build.federationEntityTypes.push(Self);

/*
* We're going to add the `@key(fields: "nodeId")` directive to this type.
* First, we need to generate an `astNode` as if the type was generateted
Expand All @@ -187,23 +235,9 @@ const AddKeyPlugin: Plugin = builder => {
// We're not changing the interfaces, so return them unmodified.
return interfaces;
});

// Add our collected types to the _Entity union
builder.hook("GraphQLUnionType:types", (types, build, context) => {
const { Self } = context;
// If it's not the _Entity union, don't change it.
if (Self.name !== "_Entity") {
return types;
}
const { federationEntityTypes } = build;

// Add our types to the entity types
return [...types, ...federationEntityTypes];
});
};

// Our federation implementation combines these two plugins:
export default makePluginByCombiningPlugins(
SchemaExtensionPlugin,
AddKeyPlugin
FederationSchemaNodeIdPlugin,
AddKeyNodeIdPlugin
);