UNPKG

@txstate-mws/graphql-server

Version:

A simple graphql server designed to work with typegraphql.

224 lines (221 loc) 9.29 kB
"use strict"; /** * Adapted from https://github.com/mercurius-js/mercurius while it was displaying an MIT license. * This adaptation is also MIT licensed. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.buildFederationSchema = buildFederationSchema; exports.ResolveReference = ResolveReference; /* eslint-disable @typescript-eslint/restrict-plus-operands */ /* eslint-disable @typescript-eslint/naming-convention */ const graphql_1 = require("graphql"); const specifiedRules_1 = require("graphql/validation/specifiedRules"); const validate_1 = require("graphql/validation/validate"); const utils_1 = require("@graphql-tools/utils"); function hasExtensionDirective(node) { for (const directive of node.directives ?? []) { if (directive === 'extends' || directive === 'requires') return true; } return false; } const BASE_FEDERATION_TYPES = ` scalar _Any scalar _FieldSet 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 directive @extends on OBJECT | INTERFACE `; const FEDERATION_SCHEMA = ` ${BASE_FEDERATION_TYPES} type _Service { sdl: String } `; const extensionKindToDefinitionKind = { [graphql_1.Kind.SCALAR_TYPE_EXTENSION]: graphql_1.Kind.SCALAR_TYPE_DEFINITION, [graphql_1.Kind.OBJECT_TYPE_EXTENSION]: graphql_1.Kind.OBJECT_TYPE_DEFINITION, [graphql_1.Kind.INTERFACE_TYPE_EXTENSION]: graphql_1.Kind.INTERFACE_TYPE_DEFINITION, [graphql_1.Kind.UNION_TYPE_EXTENSION]: graphql_1.Kind.UNION_TYPE_DEFINITION, [graphql_1.Kind.ENUM_TYPE_EXTENSION]: graphql_1.Kind.ENUM_TYPE_DEFINITION, [graphql_1.Kind.INPUT_OBJECT_TYPE_EXTENSION]: graphql_1.Kind.INPUT_OBJECT_TYPE_DEFINITION }; const definitionKindToExtensionKind = { [graphql_1.Kind.SCALAR_TYPE_DEFINITION]: graphql_1.Kind.SCALAR_TYPE_EXTENSION, [graphql_1.Kind.OBJECT_TYPE_DEFINITION]: graphql_1.Kind.OBJECT_TYPE_EXTENSION, [graphql_1.Kind.INTERFACE_TYPE_DEFINITION]: graphql_1.Kind.INTERFACE_TYPE_EXTENSION, [graphql_1.Kind.UNION_TYPE_DEFINITION]: graphql_1.Kind.UNION_TYPE_EXTENSION, [graphql_1.Kind.ENUM_TYPE_DEFINITION]: graphql_1.Kind.ENUM_TYPE_EXTENSION, [graphql_1.Kind.INPUT_OBJECT_TYPE_DEFINITION]: graphql_1.Kind.INPUT_OBJECT_TYPE_EXTENSION }; function getStubTypes(schemaDefinitions) { const definitionsMap = {}; const extensionsMap = {}; const extensions = []; const directiveDefinitions = []; for (const definition of schemaDefinitions) { const typeName = definition.name.value; const isTypeExtensionByDirective = hasExtensionDirective(definition); if ((0, graphql_1.isTypeDefinitionNode)(definition) && !isTypeExtensionByDirective) { definitionsMap[typeName] = definition; } else if ((0, graphql_1.isTypeExtensionNode)(definition) || ((0, graphql_1.isTypeDefinitionNode)(definition) && isTypeExtensionByDirective)) { extensionsMap[typeName] = { kind: isTypeExtensionByDirective ? definition.kind : extensionKindToDefinitionKind[definition.kind], name: definition.name }; if (isTypeExtensionByDirective) { definition.kind = definitionKindToExtensionKind[definition.kind]; } } else if (definition.kind === graphql_1.Kind.DIRECTIVE_DEFINITION) { directiveDefinitions.push(definition); } } return { typeStubs: Object.keys(extensionsMap) .filter(extensionTypeName => !definitionsMap[extensionTypeName]) .map(extensionTypeName => extensionsMap[extensionTypeName]), extensions, definitions: [ ...directiveDefinitions, ...Object.values(definitionsMap) ] }; } function gatherDirectives(type) { let directives = []; if (type.extensionASTNodes) { for (const node of type.extensionASTNodes) { if (node.directives) { directives = directives.concat(node.directives); } } } if (type.astNode?.directives) { directives = directives.concat(type.astNode.directives); } return directives; } function typeIncludesDirective(type, directiveName) { const directives = gatherDirectives(type); return directives.some(directive => directive.name.value === directiveName); } function addTypeNameToResult(result, typename) { if (result !== null && typeof result === 'object') { Object.defineProperty(result, '__typename', { value: typename }); } return result; } function addEntitiesResolver(schema) { const entityTypes = Object.values(schema.getTypeMap()).filter(type => (0, graphql_1.isObjectType)(type) && typeIncludesDirective(type, 'key')); if (entityTypes.length > 0) { schema = (0, graphql_1.extendSchema)(schema, (0, graphql_1.parse)(` union _Entity = ${entityTypes.join(' | ')} extend type Query { _entities(representations: [_Any!]!): [_Entity]! } `), { assumeValid: true }); const query = schema.getType('Query'); const queryFields = query.getFields(); queryFields._entities = { ...queryFields._entities, resolve: (_source, { representations }, context, info) => { return representations.map((reference) => { const { __typename } = reference; const result = resolveMap[__typename]?.(reference, {}, context, info) ?? reference; if (typeof result?.then === 'function') { return result.then((x) => addTypeNameToResult(x, __typename)); } return addTypeNameToResult(result, __typename); }); } }; } return schema; } function addServiceResolver(schema, originalSchemaSDL) { schema = (0, graphql_1.extendSchema)(schema, (0, graphql_1.parse)(` extend type Query { _service: _Service! } `), { assumeValid: true }); const query = schema.getType('Query'); const queryFields = query.getFields(); queryFields._service = { ...queryFields._service, resolve: () => ({ sdl: originalSchemaSDL }) }; return schema; } function buildFederationSchema(schema) { const originalSchemaSDL = (0, utils_1.printSchemaWithDirectives)(schema) /* these 3 lines are required only for compatibility with mercurius gateway */ .replace(/schema\s?{[\s\S]*?}\s*/, '') .replace(/^type Query/m, 'extend type Query') .replace(/^type Mutation/m, 'extend type Mutation'); const { typeStubs, extensions, definitions } = getStubTypes(Object.values(schema.getTypeMap())); let federationSchema = (0, graphql_1.extendSchema)(schema, (0, graphql_1.parse)(FEDERATION_SCHEMA), { assumeValidSDL: true }); // Add type stubs - only needed for federation federationSchema = (0, graphql_1.extendSchema)(federationSchema, { kind: graphql_1.Kind.DOCUMENT, definitions: typeStubs }, { assumeValidSDL: true }); // Add default type definitions federationSchema = (0, graphql_1.extendSchema)(federationSchema, { kind: graphql_1.Kind.DOCUMENT, definitions }, { assumeValidSDL: true }); // Add all extensions const extensionsDocument = { kind: graphql_1.Kind.DOCUMENT, definitions: extensions }; // instead of relying on extendSchema internal validations // we run validations in our code so that we can use slightly different rules // as extendSchema internal rules are meant for regular usage // and federated schemas have different constraints const errors = (0, validate_1.validateSDL)(extensionsDocument, federationSchema, specifiedRules_1.specifiedSDLRules.filter(rule => rule !== graphql_1.UniqueDirectivesPerLocationRule)); if (errors.length === 1) { throw errors[0]; } else if (errors.length > 1) { const err = new Error('Federated Schema is not valid.'); err.errors = errors; throw err; } federationSchema = (0, graphql_1.extendSchema)(federationSchema, extensionsDocument, { assumeValidSDL: true }); if (!federationSchema.getType('Query')) { federationSchema = new graphql_1.GraphQLSchema({ ...federationSchema.toConfig(), query: new graphql_1.GraphQLObjectType({ name: 'Query', fields: {} }) }); } federationSchema = addEntitiesResolver(federationSchema); federationSchema = addServiceResolver(federationSchema, originalSchemaSDL); return new graphql_1.GraphQLSchema({ ...federationSchema.toConfig(), query: federationSchema.getType('Query'), mutation: federationSchema.getType('Mutation'), subscription: federationSchema.getType('Subscription') }); } const resolveMap = {}; function ResolveReference(typename) { return (prototype, key) => { if (typeof key === 'symbol') return; resolveMap[typename] = prototype[key]; }; }