UNPKG

postgraphile-federation-plugin

Version:
300 lines (295 loc) 13.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const graphile_utils_1 = require("graphile-utils"); const printFederatedSchema_1 = (0, tslib_1.__importDefault)(require("./printFederatedSchema")); const AST_1 = require("./AST"); const defaultResolveReference_1 = require("./defaultResolveReference"); /** * 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 = (0, graphile_utils_1.makeExtendSchemaPlugin)((build) => { const { graphql: { GraphQLScalarType, getNullableType, isObjectType }, resolveNode, $$isQuery, getTypeByName, inflection, nodeIdFieldName, } = build; // Cache let Query; /** * Checks if the value is an async function. * @param value The value. * @returns A value indicating if the value is an async function. */ function isPromise(value) { return Boolean(value && "then" in value && typeof value.then === "function"); } /** * Add type name to return object. * @param maybeObject The return object. * @param typename The name of the type representation. * @returns The resolved type representation. */ function addTypeNameToPossibleReturn(maybeObject, typename) { if (maybeObject !== null && typeof maybeObject === "object") { Object.defineProperty(maybeObject, "__typename", { value: typename, }); } return maybeObject; } return { typeDefs: (0, graphile_utils_1.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 { """ The GraphQL Schema Language definition 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 @extends on OBJECT | INTERFACE 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; // eslint-disable-next-line @typescript-eslint/no-explicit-any return representations.map(async (representation) => { if (!representation || isObjectType(representation)) { throw new Error("Invalid representation"); } const { __typename, [nodeIdFieldName]: nodeId } = representation; if (!__typename) { throw new Error("Failed to interpret representation, no typename"); } if (nodeId) { // Support node interface. if (typeof nodeId !== "string") { throw new Error("Failed to interpret representation, invalid nodeId"); } return resolveNode(nodeId, build, fieldContext, data, context, resolveInfo).then((x) => addTypeNameToPossibleReturn(x, __typename)); } else { // Support non node interface keys. // Validate user-defined type against schema // to make sure it exists. const type = resolveInfo.schema.getType(__typename); if (!type || !isObjectType(type)) { throw new Error( // eslint-disable-next-line max-len `The _entities resolver tried to load an entity for type "${__typename}", but no object type of that name was found in the schema`); } const result = build.resolveReferences[type.name] ? build.resolveReferences[type.name](representation, context, resolveInfo) : build.defaultResolveReference(build, representation, context, resolveInfo); if (isPromise(result)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any return result.then((x) => addTypeNameToPossibleReturn(x, __typename)); } return addTypeNameToPossibleReturn(result, __typename); } }); }, _service(_, _args, _context, { schema }) { return schema; }, }, _Service: { sdl(schema) { return (0, printFederatedSchema_1.default)(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["__typename"]) { return getNullableType(value["__typename"]); } }, }, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore _Any: new GraphQLScalarType({ name: "_Any", serialize(value) { return value; }, }), }, }; }); /* * 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. */ const AddKeyPlugin = (builder) => { // Extend the Graphile build object. // The build object will be available throughout the // current schema build and is passed to all hooks. builder.hook("build", (build) => { // The names of entities to use for federation. build.EntityNamesToFederate = []; // The GraphQLObjectTypes to add to the _Entity union type for federation. build.graphqlObjectTypesForEntityType = []; // Add the default federation representation resolver. build.defaultResolveReference = defaultResolveReference_1.defaultResolveReference; build.resolveReferences = {}; return build; }); builder.hook("GraphQLObjectType", (type, build, context) => { const { DirectiveNode, ObjectTypeDefinitionNode } = build; const { scope: { pgIntrospection, isPgRowType }, } = context; const { inflection } = build; if (!(isPgRowType && pgIntrospection.isSelectable && pgIntrospection.namespace && pgIntrospection.primaryKeyConstraint)) { return type; } const primaryKeyNames = pgIntrospection.primaryKeyConstraint.keyAttributes.map((attr) => inflection.column(attr)); if (!primaryKeyNames.length) { return type; } const astNode = Object.assign(Object.assign({}, (0, AST_1.ObjectTypeDefinition)(type)), type.astNode); astNode.directives.push((0, AST_1.Directive)("key", { fields: (0, AST_1.StringValue)(primaryKeyNames.join(" ")) })); type.astNode = astNode; if (!build.EntityNamesToFederate.includes(type.name)) { // Add type name to list so we can use it later to get // it's GraphQLObjectType and add it to the _Entity union type. build.EntityNamesToFederate.push(type.name); } return type; }); builder.hook("GraphQLObjectType:fields", (fields, build, context) => { const { Self, scope: { isRootQuery }, } = context; // Drop the `query` field. If we don't remove it, // it will clash with other federated services `query` fields. if (isRootQuery) { const { query } = fields, rest = (0, tslib_1.__rest)(fields, ["query"]); return rest; } // Skip if not an entity we want to federate. if (!build.EntityNamesToFederate.includes(Self.name)) { return fields; } if (!build.graphqlObjectTypesForEntityType.some( // eslint-disable-next-line @typescript-eslint/no-explicit-any (e) => e.name === Self.name)) { // Add this to the list of types to be in the _Entity union. build.graphqlObjectTypesForEntityType.push(Self); } return fields; }); // Find out what types implement the Node interface builder.hook("GraphQLObjectType:interfaces", (interfaces, build, context) => { const { getTypeByName, inflection, nodeIdFieldName } = build; const { GraphQLObjectType: spec, Self, scope: { isRootQuery }, } = context; const NodeInterface = getTypeByName(inflection.builtin("Node")); /* * We only want to add federation to types that implement the Node * interface, and aren't the Query root type. */ if (isRootQuery || !NodeInterface || !interfaces.includes(NodeInterface)) { return interfaces; } // Add this to the list of types to be in the _Entity union build.graphqlObjectTypesForEntityType.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 generated * from a GraphQL SDL initially; then we assign this astNode to to the type * (via type mutation, ick) so that Apollo Federation's `printSchema` can * output it. */ const astNode = Object.assign(Object.assign({}, (0, AST_1.ObjectTypeDefinition)(spec)), Self.astNode); astNode.directives.push((0, AST_1.Directive)("key", { fields: (0, AST_1.StringValue)(nodeIdFieldName) })); Self.astNode = astNode; // 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) => { // If it's not the _Entity union, don't change it. if (context.Self.name !== "_Entity") { return types; } // Add our types to the entity types return [ ...types, ...new Set(build.graphqlObjectTypesForEntityType), // eslint-disable-next-line @typescript-eslint/no-explicit-any ]; }); }; /* * This plugin remove query/node/nodeId fields and Node interface from Query type to * fix `GraphQLSchemaValidationError: There can be only one type named "query/node/nodeId"` error. * This helps Apollo Gateway to consume two or more PostGraphile services. */ const RemoveQueryLegacyFeaturesPlugin = builder => { builder.hook('GraphQLObjectType:fields', (fields, _, context) => { const { scope: { isRootQuery }, } = context; // Deleting the query, node, nodeId fields from the Query type that are used by // the old relay specification which are not needed for modern GraphQL clients if (isRootQuery) { delete fields.query; delete fields.node; delete fields.nodeId; } return fields; }); builder.hook('GraphQLObjectType:interfaces', (interfaces, _, context) => { if (!context.scope.isRootQuery) { return interfaces; } // Delete all interfaces (i.e. the Node interface) from Query. return []; }); }; // Our federation implementation combines these two plugins: exports.default = (0, graphile_utils_1.makePluginByCombiningPlugins)(SchemaExtensionPlugin, AddKeyPlugin, RemoveQueryLegacyFeaturesPlugin); //# sourceMappingURL=index.js.map