@txstate-mws/graphql-server
Version:
A simple graphql server designed to work with typegraphql.
224 lines (221 loc) • 9.29 kB
JavaScript
;
/**
* 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];
};
}