/* @flow */ /* eslint-disable no-use-before-define, no-restricted-syntax */ import objectPath from 'object-path'; import util from 'util'; import { GraphQLInputObjectType, GraphQLNonNull, GraphQLEnumType, GraphQLObjectType, isOutputType, } from 'graphql'; import { deprecate } from './utils/debug'; import TypeMapper from './typeMapper'; import TypeComposer from './typeComposer'; import deepmerge from './utils/deepmerge'; import { resolveInputConfigsAsThunk } from './utils/configAsThunk'; import { only, clearName } from './utils/misc'; import { isFunction, isString } from './utils/is'; import { getProjectionFromAST } from './projection'; import type { GraphQLArgumentConfig, GraphQLFieldConfigArgumentMap, GraphQLOutputType, ResolverFieldConfig, ResolverMWResolve, ResolverMWResolveFn, ResolveParams, ResolverKinds, ProjectionType, ResolverFilterArgConfig, ResolverSortArgConfig, ResolverOpts, ResolverWrapFn, ResolverWrapArgsFn, ResolverWrapTypeFn, GraphQLInputType, TypeDefinitionString, TypeWrappedString, TypeNameString, } from './definition'; import InputTypeComposer from './inputTypeComposer'; import { typeByPath } from './typeByPath'; export default class Resolver { type: GraphQLOutputType; args: GraphQLFieldConfigArgumentMap; resolve: ResolverMWResolveFn; name: string; kind: ?ResolverKinds; description: ?string; parent: ?Resolver; constructor(opts: ResolverOpts = {}) { if (!opts.name) { throw new Error('For Resolver constructor the `opts.name` is required option.'); } this.name = opts.name; this.parent = opts.parent || null; this.kind = opts.kind || null; this.description = opts.description || ''; /** * @deprecated 2.0.0 */ if (opts.outputType) { deprecate('Use opts.type instead.'); // $FlowFixMe this.setType(opts.outputType); } if (opts.type) { this.setType(opts.type); } if (opts.args) { this.args = TypeMapper.convertArgConfigMap(opts.args, this.name, 'Resolver'); } else { this.args = {}; } if (opts.resolve) { this.resolve = opts.resolve; } } hasArg(argName: string): boolean { return !!this.args[argName]; } getArg(argName: string): ?GraphQLArgumentConfig { if (this.hasArg(argName)) { return this.args[argName]; } return undefined; } getArgType(argName: string): GraphQLInputType | void { const arg = this.getArg(argName); if (arg) { return arg.type; } return undefined; } getArgs(): GraphQLFieldConfigArgumentMap { return this.args; } getArgNames(): string[] { return Object.keys(this.args); } setArgs(args: GraphQLFieldConfigArgumentMap): Resolver { this.args = TypeMapper.convertArgConfigMap(args, this.name, 'Resolver'); return this; } setArg(argName: string, argConfig: GraphQLArgumentConfig): Resolver { this.args[argName] = TypeMapper.convertArgConfig(argConfig, argName, this.name, 'Resolver'); return this; } addArgs(newArgs: GraphQLFieldConfigArgumentMap): Resolver { this.setArgs(Object.assign({}, this.getArgs(), newArgs)); return this; } removeArg(argNameOrArray: string | Array): Resolver { const argNames = Array.isArray(argNameOrArray) ? argNameOrArray : [argNameOrArray]; argNames.forEach(argName => { delete this.args[argName]; }); return this; } removeOtherArgs(argNameOrArray: string | Array): Resolver { const keepArgNames = Array.isArray(argNameOrArray) ? argNameOrArray : [argNameOrArray]; Object.keys(this.args).forEach(argName => { if (!keepArgNames.includes(argName)) { delete this.args[argName]; } }); return this; } reorderArgs(names: string[]): Resolver { const orderedArgs = {}; names.forEach(name => { if (this.args[name]) { orderedArgs[name] = this.args[name]; delete this.args[name]; } }); this.args = { ...orderedArgs, ...this.args }; return this; } cloneArg(argName: string, newTypeName: string): Resolver { if (!{}.hasOwnProperty.call(this.args, argName)) { throw new Error( `Can not clone arg ${argName} for resolver ${this.name}. Argument does not exist.` ); } if (!(this.args[argName].type instanceof GraphQLInputObjectType)) { throw new Error( `Can not clone arg ${argName} for resolver ${this.name}.` + 'Argument should be GraphQLInputObjectType (complex input type).' ); } if (!newTypeName || newTypeName !== clearName(newTypeName)) { throw new Error('You should provide new type name as second argument'); } if (newTypeName === this.args[argName].type.name) { throw new Error('You should provide new type name. It is equal to current name.'); } this.args[argName] = { ...this.args[argName], type: InputTypeComposer.create(this.args[argName].type).clone(newTypeName).getType(), }; return this; } isRequired(argName: string): boolean { return this.getArgType(argName) instanceof GraphQLNonNull; } makeRequired(argNameOrArray: string | Array): Resolver { const argNames = Array.isArray(argNameOrArray) ? argNameOrArray : [argNameOrArray]; const args = this.getArgs(); argNames.forEach(argName => { if (args[argName]) { if (!(args[argName].type instanceof GraphQLNonNull)) { args[argName].type = new GraphQLNonNull(args[argName].type); } } }); this.setArgs(args); return this; } makeOptional(argNameOrArray: string | Array): Resolver { const argNames = Array.isArray(argNameOrArray) ? argNameOrArray : [argNameOrArray]; const args = this.getArgs(); argNames.forEach(argName => { if (argNames.includes(argName)) { if (args[argName].type instanceof GraphQLNonNull) { args[argName].type = args[argName].type.ofType; } } }); this.setArgs(args); return this; } /* * This method should be overriden via constructor */ // eslint-disable-next-line resolve(resolveParams: ResolveParams): Promise { return Promise.resolve(); } getResolve(): ResolverMWResolveFn { return this.resolve; } setResolve(resolve: ResolverMWResolveFn): Resolver { this.resolve = resolve; return this; } /** * @deprecated 2.0.0 */ getOutputType(): GraphQLOutputType { deprecate('Use Resolver.getType() instead.'); return this.getType(); } /** * @deprecated 2.0.0 */ setOutputType( gqType: | GraphQLOutputType | TypeDefinitionString | TypeNameString | TypeComposer | Resolver ) { deprecate('Use Resolver.setType() instead.'); this.setType(gqType); } /** * @deprecated 2.0.0 */ wrapOutputType( cb: ResolverWrapTypeFn, wrapperName: string = 'wrapType' ): Resolver { deprecate('Use Resolver.wrapType() instead.'); return this.wrapType(cb, wrapperName); } /** * @deprecated 2.0.0 */ get outputType(): GraphQLOutputType { deprecate('Use Resolver.getType() instead.'); return this.type; } /** * @deprecated 2.0.0 */ set outputType(gqType: GraphQLOutputType) { deprecate('Use Resolver.setType() instead.'); this.type = gqType; } getType(): GraphQLOutputType { return this.type; } getTypeComposer(): ?TypeComposer { if (this.type instanceof GraphQLObjectType) { return new TypeComposer(this.type); } return null; } setType( gqType: | GraphQLOutputType | TypeComposer | TypeWrappedString | TypeDefinitionString | TypeNameString | Resolver ): Resolver { const fc = TypeMapper.convertOutputFieldConfig(gqType, 'setType', 'Resolver'); // $FlowFixMe if (!fc || !isOutputType(fc.type)) { throw new Error('You should provide correct OutputType for Resolver.type.'); } // $FlowFixMe this.type = fc.type; return this; } getFieldConfig( opts: { projection?: ProjectionType, } = {} ): ResolverFieldConfig { const resolve = this.getResolve(); return { type: this.getType(), args: resolveInputConfigsAsThunk(this.getArgs()), description: this.description, resolve: (source, args, context, info) => { let projection: ProjectionType = getProjectionFromAST(info); if (opts.projection) { // $FlowFixMe projection = deepmerge(projection, opts.projection); } return resolve({ source, args, context, info, projection }); }, }; } getKind(): ?ResolverKinds { return this.kind; } setKind(kind: string): Resolver { if (kind !== 'query' && kind !== 'mutation' && kind !== 'subscription') { throw new Error( `You provide incorrect value '${kind}' for Resolver.setKind method. ` + 'Valid values are: query | mutation | subscription' ); } this.kind = kind; return this; } getDescription(): ?string { return this.description; } setDescription(description: string): Resolver { this.description = description; return this; } get(path: string | Array): mixed { // $FlowFixMe return typeByPath(this, path); } clone(opts: ResolverOpts = {}): Resolver { const oldOpts = {}; for (const key in this) { if ({}.hasOwnProperty.call(this, key)) { // $FlowFixMe oldOpts[key] = this[key]; } } oldOpts.args = Object.assign({}, this.args); return new Resolver(Object.assign({}, oldOpts, opts)); } wrap( cb: ?ResolverWrapFn, newResolverOpts: ?ResolverOpts = {} ): Resolver { const prevResolver: Resolver = this; const newResolver = this.clone({ name: 'wrap', parent: prevResolver, ...newResolverOpts, }); if (isFunction(cb)) { // $FlowFixMe cb(newResolver, prevResolver); } return newResolver; } wrapResolve( cb: ResolverMWResolve, wrapperName: string = 'wrapResolve' ): Resolver { return this.wrap( (newResolver, prevResolver) => { const newResolve = cb(prevResolver.getResolve()); newResolver.setResolve(newResolve); return newResolver; }, { name: wrapperName } ); } wrapArgs(cb: ResolverWrapArgsFn, wrapperName: string = 'wrapArgs'): Resolver { return this.wrap( (newResolver, prevResolver) => { // clone prevArgs, to avoid changing args in callback const prevArgs = Object.assign({}, prevResolver.getArgs()); const newArgs = cb(prevArgs); newResolver.setArgs(newArgs); return newResolver; }, { name: wrapperName } ); } wrapCloneArg(argName: string, newTypeName: string): Resolver { return this.wrap( newResolver => { newResolver.cloneArg(argName, newTypeName); return newResolver; }, { name: 'cloneFilterArg' } ); } wrapType(cb: ResolverWrapTypeFn, wrapperName: string = 'wrapType'): Resolver { return this.wrap( (newResolver, prevResolver) => { const prevType = prevResolver.getType(); const newType = cb(prevType); newResolver.setType(newType); return newResolver; }, { name: wrapperName } ); } addFilterArg(opts: ResolverFilterArgConfig): Resolver { if (!opts.name) { throw new Error('For Resolver.addFilterArg the arg name `opts.name` is required.'); } if (!opts.type) { throw new Error('For Resolver.addFilterArg the arg type `opts.type` is required.'); } const resolver = this.wrap(null, { name: 'addFilterArg' }); // get filterTC or create new one argument const filter = resolver.getArg('filter'); let filterITC; if (filter && filter.type instanceof GraphQLInputObjectType) { filterITC = new InputTypeComposer(filter.type); } else { if (!opts.filterTypeNameFallback || !isString(opts.filterTypeNameFallback)) { throw new Error( 'For Resolver.addFilterArg needs to provide `opts.filterTypeNameFallback: string`. ' + 'This string will be used as unique name for `filter` type of input argument. ' + 'Eg. FilterXXXXXInput' ); } filterITC = InputTypeComposer.create(opts.filterTypeNameFallback); } let defaultValue; if (filter && filter.defaultValue) { defaultValue = filter.defaultValue; } if (opts.defaultValue) { if (!defaultValue) { defaultValue = {}; } // $FlowFixMe defaultValue[opts.name] = opts.defaultValue; } resolver.setArg('filter', { type: filterITC.getType(), description: (filter && filter.description) || undefined, defaultValue, }); filterITC.setField(opts.name, { ...only(opts, ['name', 'type', 'defaultValue', 'description']), }); const resolveNext = resolver.getResolve(); if (isFunction(opts.query)) { resolver.setResolve(resolveParams => { const value = objectPath.get(resolveParams, ['args', 'filter', opts.name]); if (value !== null && value !== undefined) { if (!resolveParams.rawQuery) { resolveParams.rawQuery = {}; // eslint-disable-line } opts.query(resolveParams.rawQuery, value, resolveParams); } return resolveNext(resolveParams); }); } return resolver; } addSortArg(opts: ResolverSortArgConfig): Resolver { if (!opts.name) { throw new Error('For Resolver.addSortArg the `opts.name` is required.'); } if (!opts.value) { throw new Error('For Resolver.addSortArg the `opts.value` is required.'); } const resolver = this.wrap(null, { name: 'addSortArg' }); // get sortEnumType or create new one const sort = resolver.getArg('sort'); let sortEnumType; if (sort) { if (sort.type instanceof GraphQLEnumType) { sortEnumType = sort.type; } else { throw new Error( 'Resolver should have `sort` arg with type GraphQLEnumType. ' + `But got: ${util.inspect(sort.type, { depth: 2 })} ` ); } } else { if (!opts.sortTypeNameFallback || !isString(opts.sortTypeNameFallback)) { throw new Error( 'For Resolver.addSortArg needs to provide `opts.sortTypeNameFallback: string`. ' + 'This string will be used as unique name for `sort` type of input argument. ' + 'Eg. SortXXXXXEnum' ); } sortEnumType = new GraphQLEnumType({ name: opts.sortTypeNameFallback, values: { [opts.name]: {}, }, }); resolver.setArg('sort', { type: sortEnumType }); } // extend sortEnumType with new sorting value const existedIdx = sortEnumType._values.findIndex(o => o.name === opts.name); if (existedIdx >= 0) { sortEnumType._values.splice(existedIdx, 1); } delete sortEnumType._nameLookup; delete sortEnumType._valueLookup; sortEnumType._values.push({ name: opts.name, description: opts.description, isDeprecated: Boolean(opts.deprecationReason), deprecationReason: opts.deprecationReason, value: isFunction(opts.value) ? opts.name : opts.value, }); // If sort value is evaluable (function), then wrap resolve method const resolveNext = resolver.getResolve(); if (isFunction(opts.value)) { resolver.setResolve(resolveParams => { const value = objectPath.get(resolveParams, ['args', 'sort']); if (value === opts.name) { // $FlowFixMe const newSortValue = opts.value(resolveParams); resolveParams.args.sort = newSortValue; // eslint-disable-line } return resolveNext(resolveParams); }); } return resolver; } getNestedName() { if (this.parent) { return `${this.name}(${this.parent.getNestedName()})`; } return this.name; } toString() { function extendedInfo(resolver, spaces = '') { return [ 'Resolver(', ` name: ${resolver.name},`, ` type: ${util.inspect(resolver.type, { depth: 2 })},`, ` args: ${util.inspect(resolver.args, { depth: 3 }).replace('\n', `\n ${spaces}`)},`, ` resolve: ${resolver.resolve ? resolver.resolve .toString() .replace('\n', `\n ${spaces}`) : 'undefined'},`, ` parent: ${resolver.parent ? extendedInfo(resolver.parent, ` ${spaces}`) : ''}`, ')', ] .filter(s => !!s) .join(`\n ${spaces}`); } return extendedInfo(this); } }