import { GraphQLFieldResolver, GraphQLInterfaceType, GraphQLResolveInfo, GraphQLSchema } from 'graphql' import { PluginBuilderLens, SchemaConfig } from './builder' import { Maybe, NexusGraphQLFieldConfig, NexusGraphQLInterfaceTypeConfig, NexusGraphQLObjectTypeConfig, NexusTypes, Omit, withNexusSymbol, } from './definitions/_types' import { InputDefinitionBlock, NexusOutputFieldDef, NexusInputFieldDef } from './definitions/definitionBlocks' import { NexusInputObjectTypeConfig } from './definitions/inputObjectType' import { NexusObjectTypeConfig, ObjectDefinitionBlock } from './definitions/objectType' import { NexusSchemaExtension } from './extensions' import { isPromiseLike, PrintedGenTyping, PrintedGenTypingImport, venn } from './utils' import { NexusFinalArgConfig } from './definitions/args' export { PluginBuilderLens } export type CreateFieldResolverInfo = { /** * The internal Nexus "builder" object */ builder: PluginBuilderLens /** * Info about the GraphQL Field we're decorating. * Always guaranteed to exist, even for non-Nexus GraphQL types */ fieldConfig: Omit & { extensions?: Maybe<{ nexus?: { config: FieldExt } }> } /** * The config provided to the Nexus type containing the field. * Will not exist if this is a non-Nexus GraphQL type. */ parentTypeConfig: ( | Omit | (Omit & { interfaces: GraphQLInterfaceType[] }) ) & { extensions?: Maybe<{ nexus?: { config: TypeExt } }> } /** * The root-level SchemaConfig passed */ schemaConfig: Omit /** * Nexus specific metadata provided to the schema. */ schemaExtension: NexusSchemaExtension } export type StringLike = PrintedGenTypingImport | PrintedGenTyping | string export interface PluginConfig { /** * A name for the plugin, useful for errors, etc. */ name: string /** * A description for the plugin */ description?: string /** * Any type definitions we want to add to output field definitions */ fieldDefTypes?: StringLike | StringLike[] /** * Any type definitions we want to add to input field definitions */ inputFieldDefTypes?: StringLike | StringLike[] /** * Any type definitions we want to add to the type definition option */ objectTypeDefTypes?: StringLike | StringLike[] /** * Any type definitions we want to add to the arg definition option */ argTypeDefTypes?: StringLike | StringLike[] /** * Executed once, just before the types are walked. Useful for defining custom extensions * to the "definition" builders that are needed while traversing the type definitions, as * are defined by `dynamicOutput{Method,Property}` / `dynamicInput{Method,Property}` */ /** * Existing Description: * The plugin callback to execute when onInstall lifecycle event occurs. * OnInstall event occurs before type walking which means inline types are not * visible at this point yet. `builderLens.hasType` will only return true * for types the user has defined top level in their app, and any types added by * upstream plugins. */ onInstall?: (builder: PluginBuilderLens) => void /** * Executed once, just after types have been walked but also before the schema definition * types are materialized into GraphQL types. Use this opportunity to add / modify / remove * any types before we go through the resolution step. */ onBeforeBuild?: (builder: PluginBuilderLens) => void /** * After the schema is built, provided the Schema to do any final config validation. */ onAfterBuild?: (schema: GraphQLSchema) => void /** * Called when the `.addField` is called internally in the builder, before constructing the field */ onAddOutputField?: (field: NexusOutputFieldDef) => NexusOutputFieldDef | void /** * Called when the `.addField` is called internally in the builder, before constructing the field */ onAddInputField?: (field: NexusInputFieldDef) => NexusInputFieldDef | void /** * Called just before a Nexus arg is constructed into an GraphQLArgumentConfig */ onAddArg?: (arg: NexusFinalArgConfig) => NexusFinalArgConfig | void /** * Called immediately after the object is defined, allows for using metadata * to define the shape of the object. */ onObjectDefinition?: ( block: ObjectDefinitionBlock, objectConfig: NexusObjectTypeConfig ) => void /** * Called immediately after the input object is defined, allows for using metadata * to define the shape of the input object */ onInputObjectDefinition?: ( block: InputDefinitionBlock, objectConfig: NexusInputObjectTypeConfig ) => void /** * If a type is not defined in the schema, our plugins can register an `onMissingType` handler, * which will intercept the missing type name and give us an opportunity to respond with a valid * type. */ onMissingType?: (missingTypeName: string, builder: PluginBuilderLens) => any /** * Executed any time a field resolver is created. Returning a function here will add its in the * stack of middlewares with the (root, args, ctx, info, next) signature, where the `next` is the * next middleware or resolver to be executed. */ onCreateFieldResolver?: (createResolverInfo: CreateFieldResolverInfo) => MiddlewareFn | undefined /** * Executed any time a "subscribe" handler is created. Returning a function here will add its in the * stack of middlewares with the (root, args, ctx, info, next) signature, where the `next` is the * next middleware or resolver to be executed. */ onCreateFieldSubscribe?: (createSubscribeInfo: CreateFieldResolverInfo) => MiddlewareFn | undefined /** * Executed when a field is going to be printed to the nexus "generated types". Gives * an opportunity to override the standard behavior for printing our inferrred type info */ // onPrint?: (visitor: Visitor) => void; } /** * Helper for allowing plugins to fulfill the return of the `next` resolver, * without paying the cost of the Promise if not required. */ export function completeValue( valOrPromise: PromiseLike | T, onSuccess: (completedVal: T) => R, onError?: (errVal: any) => T ) { if (isPromiseLike(valOrPromise)) { return valOrPromise.then((completedValue) => { return onSuccess(completedValue) }, onError) } // No need to handle onError, this should just be a try/catch inside the `onSuccess` block return onSuccess(valOrPromise) } export type MiddlewareFn = ( source: any, args: any, context: any, info: GraphQLResolveInfo, next: GraphQLFieldResolver ) => any /** * Takes a list of middlewares and executes them sequentially, passing the * "next" member of the chain to execute as the 5th arg. * * @param middleware * @param resolver */ export function composeMiddlewareFns( middlewareFns: MiddlewareFn[], resolver: GraphQLFieldResolver ) { let lastResolver = resolver for (const middleware of middlewareFns.reverse()) { const currentNext = middleware const previousNext = lastResolver lastResolver = (root, args, ctx, info) => { return currentNext(root, args, ctx, info, previousNext) } } return lastResolver } /** * A definition for a plugin. Should be passed to the `plugins: []` option * on makeSchema */ export class NexusPlugin { constructor(readonly config: PluginConfig) {} } withNexusSymbol(NexusPlugin, NexusTypes.Plugin) /** * A plugin defines configuration which can document additional metadata options * for a type definition. This metadata can be used to decorate the "resolve" function * to provide custom functionality, such as logging, error handling, additional type * validation. * * You can specify options which can be defined on the schema, * the type or the plugin. The config from each of these will be * passed in during schema construction time, and used to augment the field as necessary. * * You can either return a function, with the new defintion of a resolver implementation, * or you can return an "enter" / "leave" pairing which will wrap the pre-execution of the * resolver and the "result" of the resolver, respectively. */ export function plugin(config: PluginConfig) { validatePluginConfig(config) return new NexusPlugin(config) } plugin.completeValue = completeValue // For backward compat export const createPlugin = plugin /** * Validate that the configuration given by a plugin is valid. */ function validatePluginConfig(pluginConfig: PluginConfig): void { const validRequiredProps = ['name'] const optionalPropFns: Array = [ 'onInstall', 'onCreateFieldResolver', 'onCreateFieldSubscribe', 'onBeforeBuild', 'onMissingType', 'onAfterBuild', 'onObjectDefinition', 'onAddOutputField', 'onAddInputField', 'onAddArg', 'onInputObjectDefinition', ] const validOptionalProps = [ 'description', 'fieldDefTypes', 'inputFieldDefTypes', 'objectTypeDefTypes', 'argTypeDefTypes', ...optionalPropFns, ] const validProps = [...validRequiredProps, ...validOptionalProps] const givenProps = Object.keys(pluginConfig) const printProps = (props: Iterable): string => { return [...props].join(', ') } const [missingRequiredProps, ,] = venn(validRequiredProps, givenProps) if (missingRequiredProps.size > 0) { throw new Error( `Plugin "${pluginConfig.name}" is missing required properties: ${printProps(missingRequiredProps)}` ) } const nameType = typeof pluginConfig.name if (nameType !== 'string') { throw new Error( `Plugin "${pluginConfig.name}" is giving an invalid value for property name: expected "string" type, got ${nameType} type` ) } if (pluginConfig.name === '') { throw new Error( `Plugin "${pluginConfig.name}" is giving an invalid value for property name: empty string` ) } const [, , invalidGivenProps] = venn(validProps, givenProps) if (invalidGivenProps.size > 0) { console.error( new Error( `Plugin "${pluginConfig.name}" is giving unexpected properties: ${printProps(invalidGivenProps)}` ) ) } optionalPropFns.forEach((fnName) => { const fnType = typeof pluginConfig[fnName] if (fnType !== 'function' && fnType !== 'undefined') { console.error( new Error( `Plugin "${pluginConfig.name}" is giving an invalid value for ${fnName} hook: expected "function" type, got ${fnType} type` ) ) } }) }