@nestjs/graphql
Version:
Nest - modern, fast, powerful node.js web framework (@graphql)
289 lines (288 loc) • 14.4 kB
JavaScript
var ResolversExplorerService_1;
import { __decorate, __metadata, __param } from "tslib";
import { Inject, Injectable, Logger } from '@nestjs/common';
import { isUndefined } from '@nestjs/common/utils/shared.utils.js';
import { ContextIdFactory, MetadataScanner, ModuleRef, ModulesContainer, REQUEST, } from '@nestjs/core';
import { ExternalContextCreator } from '@nestjs/core/helpers/external-context-creator.js';
import { CONTROLLER_ID_KEY } from '@nestjs/core/injector/constants.js';
import { Injector } from '@nestjs/core/injector/injector.js';
import { InternalCoreModule } from '@nestjs/core/injector/internal-core-module/index.js';
import { SerializedGraph } from '@nestjs/core/inspector/serialized-graph.js';
import { REQUEST_CONTEXT_ID } from '@nestjs/core/router/request/request-constants.js';
import { identity } from 'es-toolkit';
import { AbstractGraphQLDriver } from '../drivers/abstract-graphql.driver.js';
import { GqlParamtype } from '../enums/gql-paramtype.enum.js';
import { Resolver } from '../enums/resolver.enum.js';
import { GqlParamsFactory } from '../factories/params.factory.js';
import { FIELD_RESOLVER_MIDDLEWARE_METADATA, FIELD_TYPENAME, GRAPHQL_MODULE_OPTIONS, PARAM_ARGS_METADATA, SUBSCRIPTION_OPTIONS_METADATA, SUBSCRIPTION_TYPE, } from '../graphql.constants.js';
import { decorateFieldResolverWithMiddleware } from '../utils/decorate-field-resolver.util.js';
import { extractMetadata } from '../utils/extract-metadata.util.js';
import { createArgsMapper } from '../utils/map-args-to-props.util.js';
import { normalizeResolverArgs } from '../utils/normalize-resolver-args.js';
import { BaseExplorerService } from './base-explorer.service.js';
import { ResolverDecoratorHost } from './resolver-decorator-host.js';
const ROOT_RESOLVER_TYPES = new Set([
Resolver.MUTATION,
Resolver.QUERY,
Resolver.SUBSCRIPTION,
]);
let ResolversExplorerService = ResolversExplorerService_1 = class ResolversExplorerService extends BaseExplorerService {
constructor(modulesContainer, metadataScanner, externalContextCreator, gqlOptions, moduleRef, serializedGraph, resolverDecoratorHost) {
super();
this.modulesContainer = modulesContainer;
this.metadataScanner = metadataScanner;
this.externalContextCreator = externalContextCreator;
this.gqlOptions = gqlOptions;
this.moduleRef = moduleRef;
this.serializedGraph = serializedGraph;
this.resolverDecoratorHost = resolverDecoratorHost;
this.logger = new Logger(ResolversExplorerService_1.name);
this.gqlParamsFactory = new GqlParamsFactory();
this.injector = new Injector();
this.fieldResolverEnhancersLookup = null;
this.hasGlobalFieldMiddleware = null;
}
explore() {
const modules = this.getModules(this.modulesContainer, this.gqlOptions.include || []);
const gqlAdapter = this.moduleRef.get(AbstractGraphQLDriver);
const resolvers = this.flatMap(modules, (instance, moduleRef) => this.filterResolvers(gqlAdapter, instance, moduleRef));
return this.groupMetadata(resolvers);
}
filterResolvers(gqlAdapter, wrapper, moduleRef) {
const { instance } = wrapper;
if (!instance) {
return undefined;
}
const prototype = Object.getPrototypeOf(instance);
const predicate = (resolverType, isReferenceResolver, isPropertyResolver) => isUndefined(resolverType) ||
(!isReferenceResolver &&
!isPropertyResolver &&
!ROOT_RESOLVER_TYPES.has(resolverType));
const resolvers = this.metadataScanner
.getAllMethodNames(prototype)
.map((name) => extractMetadata(instance, prototype, name, predicate))
.filter((resolver) => !!resolver);
const isRequestScoped = !wrapper.isDependencyTreeStatic();
return resolvers.map((resolver) => {
this.assignResolverConstructorUniqueId(instance.constructor, moduleRef);
const entrypointDefinition = {
id: `${wrapper.id}_${resolver.methodName}`,
type: 'graphql-entrypoint',
methodName: resolver.methodName,
className: wrapper.name,
classNodeId: wrapper.id,
metadata: {
key: resolver.name,
parentType: resolver.type,
},
};
this.serializedGraph.insertEntrypoint(entrypointDefinition, wrapper.id);
const createContext = (transform) => this.createContextCallback(instance, prototype, wrapper, moduleRef, resolver, isRequestScoped, transform);
if (resolver.type === SUBSCRIPTION_TYPE) {
if (!wrapper.isDependencyTreeStatic()) {
// Note: We don't throw an exception here for backward
// compatibility reasons.
this.logger.error(`"${wrapper.metatype.name}" resolver is request or transient-scoped. Resolvers that register subscriptions with the "@Subscription()" decorator must be static (singleton).`);
}
const subscriptionOptions = Reflect.getMetadata(SUBSCRIPTION_OPTIONS_METADATA, instance[resolver.methodName]);
return this.createSubscriptionMetadata(gqlAdapter, createContext, subscriptionOptions, resolver, instance);
}
return {
...resolver,
callback: createContext(),
};
});
}
createContextCallback(instance, prototype, wrapper, moduleRef, resolver, isRequestScoped, transform = identity) {
const paramsFactory = this.gqlParamsFactory;
const isPropertyResolver = !ROOT_RESOLVER_TYPES.has(resolver.type);
const mapArgs = createArgsMapper(instance.constructor, resolver.methodName);
if (this.fieldResolverEnhancersLookup === null) {
const enhancers = this.gqlOptions.fieldResolverEnhancers || [];
this.fieldResolverEnhancersLookup = {
guards: enhancers.includes('guards'),
filters: enhancers.includes('filters'),
interceptors: enhancers.includes('interceptors'),
};
}
const contextOptions = resolver.methodName === FIELD_TYPENAME
? { guards: false, filters: false, interceptors: false }
: isPropertyResolver
? this.fieldResolverEnhancersLookup
: undefined;
if (isRequestScoped) {
const resolverCallback = async (...args) => {
const gqlContext = paramsFactory.exchangeKeyForValue(GqlParamtype.CONTEXT, undefined, args);
const contextId = this.getContextId(gqlContext);
this.registerContextProvider(gqlContext, contextId);
const contextInstance = await this.injector.loadPerContext(instance, moduleRef, moduleRef.providers, contextId);
const callback = this.externalContextCreator.create(contextInstance, transform(contextInstance[resolver.methodName]), resolver.methodName, PARAM_ARGS_METADATA, paramsFactory, contextId, wrapper.id, contextOptions, 'graphql');
return callback(...args);
};
const wrappedCallback = this.withMappedArgs(resolverCallback, mapArgs);
return isPropertyResolver
? this.registerFieldMiddlewareIfExists(wrappedCallback, instance, resolver.methodName)
: this.resolverDecoratorHost.decorate(wrappedCallback);
}
if (isPropertyResolver &&
this.canUseFastFieldResolver(instance, resolver.methodName, contextOptions)) {
const resolverFn = prototype[resolver.methodName];
if (typeof resolverFn === 'function') {
return resolverFn.bind(instance);
}
}
const resolverCallback = this.externalContextCreator.create(instance, prototype[resolver.methodName], resolver.methodName, PARAM_ARGS_METADATA, paramsFactory, undefined, undefined, contextOptions, 'graphql');
const wrappedCallback = this.withMappedArgs(resolverCallback, mapArgs);
return isPropertyResolver
? this.registerFieldMiddlewareIfExists(wrappedCallback, instance, resolver.methodName)
: this.resolverDecoratorHost.decorate(wrappedCallback);
}
/**
* Rewrites the incoming arguments object onto the property names declared by
* the `@ArgsType()`/`@InputType()` classes of the method before the resolver
* context (and therefore any pipe, global ones included) runs.
*/
withMappedArgs(resolverFn, mapArgs) {
if (!mapArgs) {
return resolverFn;
}
return ((...args) => {
// Reference resolvers and "resolveType" callbacks receive no arguments
// object, which is exactly what normalizeResolverArgs() detects.
if (args.length < 2 || normalizeResolverArgs(args) !== args) {
return resolverFn(...args);
}
const mappedArgs = mapArgs(args[1]);
if (mappedArgs === args[1]) {
return resolverFn(...args);
}
const nextArgs = [...args];
nextArgs[1] = mappedArgs;
return resolverFn(...nextArgs);
});
}
createSubscriptionMetadata(gqlAdapter, createSubscribeContext, subscriptionOptions, resolverMetadata, instanceRef) {
const resolveFunc = subscriptionOptions &&
subscriptionOptions.resolve &&
subscriptionOptions.resolve.bind(instanceRef);
const baseCallbackMetadata = {
resolve: resolveFunc,
};
if (subscriptionOptions && subscriptionOptions.filter) {
return {
...resolverMetadata,
callback: {
...baseCallbackMetadata,
subscribe: gqlAdapter.subscriptionWithFilter(instanceRef, subscriptionOptions.filter, createSubscribeContext),
},
};
}
return {
...resolverMetadata,
callback: {
...baseCallbackMetadata,
subscribe: createSubscribeContext(),
},
};
}
getAllCtors() {
const modules = this.getModules(this.modulesContainer, this.gqlOptions.include || []);
const resolvers = this.flatMap(modules, this.mapToCtor).filter(Boolean);
return resolvers;
}
mapToCtor(wrapper) {
const { instance } = wrapper;
if (!instance) {
return undefined;
}
return instance.constructor;
}
registerContextProvider(request, contextId) {
if (this.coreModuleRef === undefined) {
let foundModule = null;
for (const [, moduleRef] of this.modulesContainer.entries()) {
if (moduleRef.metatype?.name === InternalCoreModule.name) {
foundModule = moduleRef;
break;
}
}
this.coreModuleRef = foundModule;
}
if (!this.coreModuleRef) {
return;
}
const wrapper = this.coreModuleRef.getProviderByKey(REQUEST);
wrapper.setInstanceByContextId(contextId, {
instance: contextId.getParent ? contextId.payload : request,
isResolved: true,
});
}
registerFieldMiddlewareIfExists(resolverFn, instance, methodKey) {
const fieldMiddleware = Reflect.getMetadata(FIELD_RESOLVER_MIDDLEWARE_METADATA, instance[methodKey]);
const middlewareFunctions = (this.gqlOptions?.buildSchemaOptions?.fieldMiddleware || []).concat(fieldMiddleware || []);
if (middlewareFunctions?.length === 0) {
return resolverFn;
}
const originalResolveFnFactory = (...args) => () => resolverFn(...args);
return decorateFieldResolverWithMiddleware(originalResolveFnFactory, middlewareFunctions);
}
getContextId(gqlContext) {
const contextId = ContextIdFactory.getByRequest(gqlContext, ['req']);
if (!gqlContext[REQUEST_CONTEXT_ID]) {
Object.defineProperty(gqlContext, REQUEST_CONTEXT_ID, {
value: contextId,
enumerable: false,
configurable: false,
writable: false,
});
}
return contextId;
}
/**
* Determines if a field resolver can use the fast-path that bypasses
* ExternalContextCreator overhead. This is possible when:
* - No guards/filters/interceptors are enabled for field resolvers
* - No field middleware is registered (global or method-level)
* - No parameter decorators (@Parent, @Args, etc.) are used on the method
*/
canUseFastFieldResolver(instance, methodKey, contextOptions) {
if (contextOptions?.guards ||
contextOptions?.filters ||
contextOptions?.interceptors) {
return false;
}
const fieldMiddleware = Reflect.getMetadata(FIELD_RESOLVER_MIDDLEWARE_METADATA, instance[methodKey]);
if (fieldMiddleware?.length > 0) {
return false;
}
if (this.hasGlobalFieldMiddleware === null) {
const globalMiddleware = this.gqlOptions?.buildSchemaOptions?.fieldMiddleware;
this.hasGlobalFieldMiddleware = (globalMiddleware?.length ?? 0) > 0;
}
if (this.hasGlobalFieldMiddleware) {
return false;
}
const paramMetadata = Reflect.getMetadata(PARAM_ARGS_METADATA, instance.constructor, methodKey);
if (paramMetadata && Object.keys(paramMetadata).length > 0) {
return false;
}
return true;
}
assignResolverConstructorUniqueId(resolverConstructor, moduleRef) {
// eslint-disable-next-line no-prototype-builtins
if (resolverConstructor.hasOwnProperty(CONTROLLER_ID_KEY)) {
return;
}
moduleRef.assignControllerUniqueId(resolverConstructor);
}
};
ResolversExplorerService = ResolversExplorerService_1 = __decorate([
Injectable(),
__param(3, Inject(GRAPHQL_MODULE_OPTIONS)),
__metadata("design:paramtypes", [ModulesContainer,
MetadataScanner,
ExternalContextCreator, Object, ModuleRef,
SerializedGraph,
ResolverDecoratorHost])
], ResolversExplorerService);
export { ResolversExplorerService };