UNPKG

@nestjs/graphql

Version:

Nest - modern, fast, powerful node.js web framework (@graphql)

289 lines (288 loc) 14.4 kB
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 };