@nestjs/graphql
Version:
Nest - modern, fast, powerful node.js web framework (@graphql)
314 lines (313 loc) • 14.9 kB
JavaScript
import { __decorate, __metadata } from "tslib";
import { mergeSchemas, addResolversToSchema } from '@graphql-tools/schema';
import { printSchemaWithDirectives } from '@graphql-tools/utils';
import { Injectable } from '@nestjs/common';
import { loadPackage, loadPackageSync, } from '@nestjs/common/utils/load-package.util.js';
import { isString } from '@nestjs/common/utils/shared.utils.js';
import { DirectiveLocation, GraphQLDirective, GraphQLInterfaceType, GraphQLObjectType, GraphQLUnionType, isEnumType, isInputObjectType, isInterfaceType, isObjectType, isScalarType, isUnionType, specifiedDirectives, } from 'graphql';
import { gql } from 'graphql-tag';
import { createRequire } from 'module';
import { GraphQLSchemaBuilder } from '../graphql-schema.builder.js';
import { ResolversExplorerService, ScalarsExplorerService, } from '../services/index.js';
import { extend } from '../utils/index.js';
import { backfillDefaultValues } from '../utils/backfill-default-values.util.js';
import { transformSchema } from '../utils/transform-schema.util.js';
import { TypeDefsDecoratorFactory } from './type-defs-decorator.factory.js';
const DEFAULT_FEDERATION_VERSION = 1;
const nodeRequire = createRequire(import.meta.url);
/**
* @publicApi
*/
let GraphQLFederationFactory = class GraphQLFederationFactory {
constructor(resolversExplorerService, scalarsExplorerService, gqlSchemaBuilder, typeDefsDecoratorFactory) {
this.resolversExplorerService = resolversExplorerService;
this.scalarsExplorerService = scalarsExplorerService;
this.gqlSchemaBuilder = gqlSchemaBuilder;
this.typeDefsDecoratorFactory = typeDefsDecoratorFactory;
}
async generateSchema(options = {}, buildFederatedSchema) {
const transformSchema = options.transformSchema ?? ((schema) => schema);
let schema;
if (options.autoSchemaFile) {
schema = await this.generateSchemaFromCodeFirst(options, buildFederatedSchema);
}
else if (this.isEmptyValue(options.typeDefs)) {
schema = options.schema;
}
else {
schema = await this.buildSchemaFromTypeDefs(options);
}
return await transformSchema(schema);
}
async buildSchemaFromTypeDefs(options) {
const { buildSubgraphSchema } = await loadPackage('@apollo/subgraph', 'ApolloFederation', () => import('@apollo/subgraph'));
const resolvers = this.getResolvers(options.resolvers);
return addResolversToSchema({
resolverValidationOptions: options.resolverValidationOptions,
inheritResolversFromInterfaces: options.inheritResolversFromInterfaces,
resolvers,
schema: buildSubgraphSchema([
{
typeDefs: gql `
${options.typeDefs}
`,
resolvers,
},
]),
});
}
async generateSchemaFromCodeFirst(options, buildFederatedSchema) {
const { subgraph: apolloSubgraph, majorVersion: apolloSubgraphMajorVersion, } = await this.loadApolloSubgraph();
const printSubgraphSchema = apolloSubgraph.printSubgraphSchema;
if (!buildFederatedSchema) {
buildFederatedSchema = apolloSubgraph.buildSubgraphSchema;
}
const autoGeneratedSchema = await this.buildFederatedSchema(options.autoSchemaFile, options, this.resolversExplorerService.getAllCtors());
let typeDefs = apolloSubgraphMajorVersion >= 2
? printSchemaWithDirectives(backfillDefaultValues(autoGeneratedSchema))
: printSubgraphSchema(autoGeneratedSchema);
const [federationVersion, federationOptions] = this.getFederationVersionAndConfig(options.autoSchemaFile);
const typeDefsDecorator = this.typeDefsDecoratorFactory.create(federationVersion, apolloSubgraphMajorVersion);
if (typeDefsDecorator) {
typeDefs = typeDefsDecorator.decorate(typeDefs, federationOptions);
}
const resolvers = this.getResolvers(options.resolvers);
let executableSchema = addResolversToSchema({
schema: buildFederatedSchema({
typeDefs: gql(typeDefs),
resolvers,
}),
resolvers,
resolverValidationOptions: options.resolverValidationOptions,
inheritResolversFromInterfaces: options.inheritResolversFromInterfaces,
});
executableSchema = this.overrideOrExtendResolvers(executableSchema, autoGeneratedSchema, printSubgraphSchema);
const schema = options.schema
? mergeSchemas({
schemas: [options.schema, executableSchema],
})
: executableSchema;
return schema;
}
getResolvers(optionResolvers) {
optionResolvers = Array.isArray(optionResolvers)
? optionResolvers
: [optionResolvers];
return this.extendResolvers([
this.resolversExplorerService.explore(),
...this.scalarsExplorerService.explore(),
...optionResolvers,
]);
}
extendResolvers(resolvers) {
return resolvers.reduce((prev, curr) => extend(prev, curr), {});
}
overrideOrExtendResolvers(executableSchema, autoGeneratedSchema, printSchema) {
return transformSchema(executableSchema, (type) => {
if (isUnionType(type) && type.name !== '_Entity') {
return this.overrideFederatedResolveType(type, autoGeneratedSchema);
}
else if (isInterfaceType(type)) {
return this.overrideFederatedResolveType(type, autoGeneratedSchema);
}
else if (isEnumType(type)) {
return autoGeneratedSchema.getType(type.name);
}
else if (isInputObjectType(type)) {
const autoGeneratedInputType = autoGeneratedSchema.getType(type.name);
if (!autoGeneratedInputType) {
return type;
}
const fields = type.getFields();
Object.entries(fields).forEach(([key, value]) => {
const field = autoGeneratedInputType.getFields()[key];
if (!field) {
return;
}
value.extensions = field.extensions;
value.astNode = field.astNode;
});
type.extensions = autoGeneratedInputType.extensions;
return type;
}
else if (isObjectType(type)) {
const autoGeneratedObjectType = autoGeneratedSchema.getType(type.name);
if (!autoGeneratedObjectType) {
return type;
}
const fields = type.getFields();
Object.entries(fields).forEach(([key, value]) => {
const field = autoGeneratedObjectType.getFields()[key];
if (!field) {
return;
}
value.extensions = field.extensions;
value.astNode = field.astNode;
if (!value.resolve) {
value.resolve = field.resolve;
}
});
if (autoGeneratedObjectType.astNode) {
type.astNode = {
...type.astNode,
...autoGeneratedObjectType.astNode,
};
}
type.extensions = {
...type.extensions,
...autoGeneratedObjectType.extensions,
};
return type;
}
else if (isScalarType(type) &&
(type.name === 'DateTime' || type.name === 'Timestamp')) {
const autoGeneratedScalar = autoGeneratedSchema.getType(type.name);
if (!autoGeneratedScalar) {
return type;
}
type.parseLiteral = autoGeneratedScalar.parseLiteral;
type.parseValue = autoGeneratedScalar.parseValue;
type.serialize = autoGeneratedScalar.serialize;
// graphql v17 coerces values through `coerce*` methods, which are
// separate from their deprecated `serialize`/`parse*` counterparts.
copyCoercionMethods(autoGeneratedScalar, type);
return type;
}
return type;
});
}
/**
* Ensures that the resolveType method for unions and interfaces in the federated schema
* is properly set from the one in the autoGeneratedSchema.
*/
overrideFederatedResolveType(typeInFederatedSchema, autoGeneratedSchema) {
// Get the matching type from the auto generated schema
const autoGeneratedType = autoGeneratedSchema.getType(typeInFederatedSchema.name);
// Bail if inconsistent with original schema
if (!autoGeneratedType ||
!(autoGeneratedType instanceof GraphQLUnionType ||
autoGeneratedType instanceof GraphQLInterfaceType) ||
!autoGeneratedType.resolveType) {
return typeInFederatedSchema;
}
typeInFederatedSchema.resolveType = async (value, context, info, abstractType) => {
const resultFromAutogenSchema = await autoGeneratedType.resolveType(value, context, info, abstractType);
// If the result is not a GraphQLObjectType we're fine
if (!resultFromAutogenSchema || isString(resultFromAutogenSchema)) {
return resultFromAutogenSchema;
}
// We now have a GraphQLObjectType from the original union in the autogenerated schema.
// But we can't return that without the additional federation property apollo adds to object
// types (see node_modules/@apollo/federation/src/composition/types.ts:47).
// Without that property, Apollo will ignore the returned type and the
// union value will resolve to null. So we need to return the type with
// the same name from the federated schema
const resultFromFederatedSchema = info.schema.getType(resultFromAutogenSchema.name);
if (resultFromFederatedSchema &&
resultFromFederatedSchema instanceof GraphQLObjectType) {
return resultFromFederatedSchema;
}
// If we couldn't find a match in the federated schema, return just the
// name of the type and hope apollo works it out
return resultFromAutogenSchema;
};
return typeInFederatedSchema;
}
async buildFederatedSchema(autoSchemaFile, options, resolvers) {
const scalarsMap = this.scalarsExplorerService.getScalarsMap();
try {
const buildSchemaOptions = options.buildSchemaOptions || {};
const directives = [...specifiedDirectives];
const [federationVersion] = this.getFederationVersionAndConfig(autoSchemaFile);
if (federationVersion < 2) {
directives.push(...this.loadFederationDirectives());
}
if (buildSchemaOptions?.directives) {
directives.push(...buildSchemaOptions.directives);
}
return await this.gqlSchemaBuilder.generateSchema(resolvers, autoSchemaFile, {
...buildSchemaOptions,
directives,
scalarsMap,
skipCheck: true,
}, options.sortSchema, options.transformAutoSchemaFile && options.transformSchema, await this.getFederationSchemaPrinter());
}
catch (err) {
if (err && err.details) {
console.error(err.details);
}
throw err;
}
}
async loadApolloSubgraph() {
const subgraph = await loadPackage('@apollo/subgraph', 'ApolloFederation', () => import('@apollo/subgraph'));
const { version } = nodeRequire('@apollo/subgraph/package.json');
return { subgraph, majorVersion: Number(version.split('.')[0]) };
}
async getFederationSchemaPrinter() {
const { subgraph, majorVersion } = await this.loadApolloSubgraph();
return majorVersion >= 2
? (schema) => printSchemaWithDirectives(schema)
: subgraph.printSubgraphSchema;
}
getFederationVersionAndConfig(autoSchemaFile) {
if (!autoSchemaFile || typeof autoSchemaFile !== 'object') {
return [DEFAULT_FEDERATION_VERSION];
}
if (typeof autoSchemaFile.federation !== 'object') {
return [autoSchemaFile.federation ?? DEFAULT_FEDERATION_VERSION];
}
return [
autoSchemaFile.federation?.version ?? DEFAULT_FEDERATION_VERSION,
autoSchemaFile.federation,
];
}
loadFederationDirectives() {
const { federationDirectives, directivesWithNoDefinitionNeeded } = loadPackageSync('@apollo/subgraph/dist/directives', 'SchemaBuilder', () => nodeRequire('@apollo/subgraph/dist/directives'));
const directives = federationDirectives ?? directivesWithNoDefinitionNeeded;
// "@apollo/subgraph" >= 2.13 declares "@tag" on SCHEMA as well, but that location
// was only introduced by the Federation 2 tag spec. Since these definitions are
// inlined into Federation 1 schemas, drop it to keep such schemas valid.
return directives.map((directive) => {
if (directive.name !== 'tag' ||
!directive.locations.includes(DirectiveLocation.SCHEMA)) {
return directive;
}
const config = directive.toConfig();
return new GraphQLDirective({
...config,
locations: config.locations.filter((location) => location !== DirectiveLocation.SCHEMA),
});
});
}
isEmptyValue(value) {
if (value == null) {
return true;
}
if (Array.isArray(value) || typeof value === 'string') {
return value.length === 0;
}
return false;
}
};
GraphQLFederationFactory = __decorate([
Injectable(),
__metadata("design:paramtypes", [ResolversExplorerService,
ScalarsExplorerService,
GraphQLSchemaBuilder,
TypeDefsDecoratorFactory])
], GraphQLFederationFactory);
export { GraphQLFederationFactory };
function copyCoercionMethods(source, target) {
const sourceMethods = source;
const targetMethods = target;
for (const method of [
'coerceOutputValue',
'coerceInputValue',
'coerceInputLiteral',
]) {
if (typeof sourceMethods[method] === 'function') {
targetMethods[method] = sourceMethods[method];
}
}
}