function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } /* eslint-disable no-use-before-define */ import { GraphQLInterfaceType, GraphQLObjectType, GraphQLList, GraphQLNonNull, getNamedType } from './graphql'; import { isObject, isString, isFunction } from './utils/is'; import { resolveMaybeThunk, inspect } from './utils/misc'; import { ObjectTypeComposer, isComposeOutputType } from './ObjectTypeComposer'; import { InputTypeComposer } from './InputTypeComposer'; import { UnionTypeComposer } from './UnionTypeComposer'; import { SchemaComposer } from './SchemaComposer'; import { resolveOutputConfigMapAsThunk, resolveOutputConfigAsThunk, resolveArgConfigAsThunk } from './utils/configAsThunk'; import { toInputObjectType } from './utils/toInputObjectType'; import { typeByPath } from './utils/typeByPath'; import { getGraphQLType } from './utils/typeHelpers'; import { defineFieldMap, defineFieldMapToConfig } from './utils/configToDefine'; import { graphqlVersion } from './utils/graphqlVersion'; export class InterfaceTypeComposer { // Also supported `GraphQLInterfaceType` but in such case Flowtype force developers // to explicitly write annotations in their code. But it's bad. static create(typeDef, schemaComposer) { if (!(schemaComposer instanceof SchemaComposer)) { throw new Error('You must provide SchemaComposer instance as a second argument for `InterfaceTypeComposer.create(typeDef, schemaComposer)`'); } const iftc = this.createTemp(typeDef, schemaComposer); schemaComposer.add(iftc); return iftc; } static createTemp(typeDef, schemaComposer) { const sc = schemaComposer || new SchemaComposer(); let IFTC; if (isString(typeDef)) { const typeName = typeDef; const NAME_RX = /^[_a-zA-Z][_a-zA-Z0-9]*$/; if (NAME_RX.test(typeName)) { IFTC = new InterfaceTypeComposer(new GraphQLInterfaceType({ name: typeName, fields: () => ({}) }), sc); } else { IFTC = sc.typeMapper.createType(typeName); if (!(IFTC instanceof InterfaceTypeComposer)) { throw new Error('You should provide correct GraphQLInterfaceType type definition.' + 'Eg. `interface MyType { id: ID!, name: String! }`'); } } } else if (typeDef instanceof GraphQLInterfaceType) { IFTC = new InterfaceTypeComposer(typeDef, sc); } else if (isObject(typeDef)) { const fields = typeDef.fields; const type = new GraphQLInterfaceType(_objectSpread({}, typeDef, { fields: isFunction(fields) ? () => resolveOutputConfigMapAsThunk(sc, fields(), typeDef.name) : () => ({}) })); IFTC = new InterfaceTypeComposer(type, sc); if (isObject(typeDef.fields)) IFTC.addFields(typeDef.fields); IFTC.gqType._gqcExtensions = typeDef.extensions || {}; } else { throw new Error(`You should provide GraphQLInterfaceTypeConfig or string with interface name or SDL definition. Provided:\n${inspect(typeDef)}`); } return IFTC; } constructor(gqType, schemaComposer) { if (!(schemaComposer instanceof SchemaComposer)) { throw new Error('You must provide SchemaComposer instance as a second argument for `new InterfaceTypeComposer(GraphQLInterfaceType, SchemaComposer)`'); } this.schemaComposer = schemaComposer; if (!(gqType instanceof GraphQLInterfaceType)) { throw new Error('InterfaceTypeComposer accept only GraphQLInterfaceType in constructor'); } this.gqType = gqType; // alive proper Flow type casting in autosuggestions for class with Generics /* :: return this; */ } // ----------------------------------------------- // Field methods // ----------------------------------------------- hasField(name) { const fields = this.getFields(); return !!fields[name]; } getFields() { if (!this.gqType._gqcFields) { if (graphqlVersion >= 14) { this.gqType._gqcFields = defineFieldMapToConfig(this.gqType._fields); } else { const fields = this.gqType._typeConfig.fields; this.gqType._gqcFields = resolveMaybeThunk(fields) || {}; } } return this.gqType._gqcFields; } getField(fieldName) { const fields = this.getFields(); let field = fields[fieldName]; if (!field) { throw new Error(`Cannot get field '${fieldName}' from type '${this.getTypeName()}'. Field does not exist.`); } if (isFunction(field)) field = field(); if (typeof field === 'string' || isComposeOutputType(field) || Array.isArray(field)) { return { type: field }; } return field; } getFieldNames() { return Object.keys(this.getFields()); } setFields(fields) { this.gqType._gqcFields = fields; if (graphqlVersion >= 14) { this.gqType._fields = () => { return defineFieldMap(this.gqType, resolveOutputConfigMapAsThunk(this.schemaComposer, fields, this.getTypeName())); }; } else { this.gqType._typeConfig.fields = () => { return resolveOutputConfigMapAsThunk(this.schemaComposer, fields, this.getTypeName()); }; delete this.gqType._fields; // clear builded fields in type } return this; } setField(name, fieldConfig) { this.addFields({ [name]: fieldConfig }); return this; } /** * Add new fields or replace existed in a GraphQL type */ addFields(newValues) { this.setFields(_objectSpread({}, this.getFields(), newValues)); return this; } removeField(nameOrArray) { const fieldNames = Array.isArray(nameOrArray) ? nameOrArray : [nameOrArray]; const values = this.getFields(); fieldNames.forEach(valueName => delete values[valueName]); this.setFields(_objectSpread({}, values)); return this; } removeOtherFields(fieldNameOrArray) { const keepFieldNames = Array.isArray(fieldNameOrArray) ? fieldNameOrArray : [fieldNameOrArray]; const fields = this.getFields(); Object.keys(fields).forEach(fieldName => { if (keepFieldNames.indexOf(fieldName) === -1) { delete fields[fieldName]; } }); this.setFields(fields); return this; } reorderFields(names) { const orderedFields = {}; const fields = this.getFields(); names.forEach(name => { if (fields[name]) { orderedFields[name] = fields[name]; delete fields[name]; } }); this.setFields(_objectSpread({}, orderedFields, fields)); return this; } extendField(fieldName, partialFieldConfig) { let prevFieldConfig; try { prevFieldConfig = this.getField(fieldName); } catch (e) { throw new Error(`Cannot extend field '${fieldName}' from type '${this.getTypeName()}'. Field does not exist.`); } this.setField(fieldName, _objectSpread({}, prevFieldConfig, partialFieldConfig, { extensions: _objectSpread({}, prevFieldConfig.extensions || {}, partialFieldConfig.extensions || {}) })); return this; } isFieldNonNull(fieldName) { return this.getFieldType(fieldName) instanceof GraphQLNonNull; } getFieldConfig(fieldName) { const fc = this.getField(fieldName); if (!fc) { throw new Error(`Type ${this.getTypeName()} does not have field with name '${fieldName}'`); } return resolveOutputConfigAsThunk(this.schemaComposer, fc, fieldName, this.getTypeName()); } getFieldType(fieldName) { return this.getFieldConfig(fieldName).type; } getFieldTC(fieldName) { const fieldType = getNamedType(this.getFieldType(fieldName)); const tc = this.schemaComposer.createTC(fieldType); if (tc instanceof InputTypeComposer) { throw new Error(`${this.getTypeName()}.getFieldTC('${fieldName}') returns InputTypeComposer. It's very strange cause fields may have only Output types (Scalar, Object, Enum, Union, Interface).`); } return tc; } /** * Alias for `getFieldTC()` but returns statically checked ObjectTypeComposer. * If field have other type then error will be thrown. */ getFieldOTC(fieldName) { const tc = this.getFieldTC(fieldName); if (!(tc instanceof ObjectTypeComposer)) { throw new Error(`${this.getTypeName()}.getFieldOTC('${fieldName}') must be ObjectTypeComposer, but recieved ${tc.constructor.name}. Maybe you need to use 'getFieldTC()' method which returns any type composer?`); } return tc; } makeFieldNonNull(fieldNameOrArray) { const fieldNames = Array.isArray(fieldNameOrArray) ? fieldNameOrArray : [fieldNameOrArray]; fieldNames.forEach(fieldName => { if (this.hasField(fieldName)) { const fieldType = this.getFieldType(fieldName); if (!(fieldType instanceof GraphQLNonNull)) { this.extendField(fieldName, { type: new GraphQLNonNull(fieldType) }); } } }); return this; } makeFieldNullable(fieldNameOrArray) { const fieldNames = Array.isArray(fieldNameOrArray) ? fieldNameOrArray : [fieldNameOrArray]; fieldNames.forEach(fieldName => { if (this.hasField(fieldName)) { const fieldType = this.getFieldType(fieldName); if (fieldType instanceof GraphQLNonNull) { this.extendField(fieldName, { type: fieldType.ofType }); } } }); return this; } deprecateFields(fields) { const existedFieldNames = this.getFieldNames(); if (typeof fields === 'string') { if (existedFieldNames.indexOf(fields) === -1) { throw new Error(`Cannot deprecate unexisted field '${fields}' from interface type '${this.getTypeName()}'`); } this.extendField(fields, { deprecationReason: 'deprecated' }); } else if (Array.isArray(fields)) { fields.forEach(field => { if (existedFieldNames.indexOf(field) === -1) { throw new Error(`Cannot deprecate unexisted field '${field}' from interface type '${this.getTypeName()}'`); } this.extendField(field, { deprecationReason: 'deprecated' }); }); } else { const fieldMap = fields; Object.keys(fieldMap).forEach(field => { if (existedFieldNames.indexOf(field) === -1) { throw new Error(`Cannot deprecate unexisted field '${field}' from interface type '${this.getTypeName()}'`); } const deprecationReason = fieldMap[field]; this.extendField(field, { deprecationReason }); }); } return this; } getFieldArgs(fieldName) { try { const fc = this.getField(fieldName); return fc.args || {}; } catch (e) { throw new Error(`Cannot get field args. Field '${fieldName}' from type '${this.getTypeName()}' does not exist.`); } } hasFieldArg(fieldName, argName) { try { const fieldArgs = this.getFieldArgs(fieldName); return !!fieldArgs[argName]; } catch (e) { return false; } } getFieldArg(fieldName, argName) { const fieldArgs = this.getFieldArgs(fieldName); let arg = fieldArgs[argName]; if (!arg) { throw new Error(`Cannot get arg '${argName}' from type.field '${this.getTypeName()}.${fieldName}'. Argument does not exist.`); } if (isFunction(arg)) arg = arg(); if (typeof arg === 'string' || isComposeOutputType(arg) || Array.isArray(arg)) { return { type: arg }; } return arg; } getFieldArgType(fieldName, argName) { const ac = this.getFieldArg(fieldName, argName); const graphqlAC = resolveArgConfigAsThunk(this.schemaComposer, ac, argName, fieldName, this.getTypeName()); return graphqlAC.type; } getFieldArgTC(fieldName, argName) { const fieldType = getNamedType(this.getFieldArgType(fieldName, argName)); const tc = this.schemaComposer.createTC(fieldType); if (tc instanceof ObjectTypeComposer || tc instanceof InterfaceTypeComposer || tc instanceof UnionTypeComposer) { throw new Error(`${this.getTypeName()}.getFieldArgTC('${fieldName}', '${argName}') returns ${tc.constructor.name}. It's very strange cause args may have only Input types (Scalar, InputObject, Enum).`); } return tc; } /** * Alias for `getFieldArgTC()` but returns statically checked InputTypeComposer. * If field have other type then error will be thrown. */ getFieldArgITC(fieldName, argName) { const tc = this.getFieldArgTC(fieldName, argName); if (!(tc instanceof InputTypeComposer)) { throw new Error(`${this.getTypeName()}.getFieldArgITC('${fieldName}', '${argName}') must be InputTypeComposer, but recieved ${tc.constructor.name}. Maybe you need to use 'getFieldArgTC()' method which returns any type composer?`); } return tc; } setFieldArgs(fieldName, args) { const field = _objectSpread({}, this.getField(fieldName)); field.args = args; this.setField(fieldName, field); return this; } addFieldArgs(fieldName, newArgs) { this.setFieldArgs(fieldName, _objectSpread({}, this.getFieldArgs(fieldName), newArgs)); return this; } setFieldArg(fieldName, argName, argConfig) { this.addFieldArgs(fieldName, { [argName]: argConfig }); return this; } // ----------------------------------------------- // Type methods // ----------------------------------------------- getType() { return this.gqType; } getTypePlural() { return new GraphQLList(this.gqType); } getTypeNonNull() { return new GraphQLNonNull(this.gqType); } getTypeName() { return this.gqType.name; } setTypeName(name) { this.gqType.name = name; this.schemaComposer.add(this); return this; } getDescription() { return this.gqType.description || ''; } setDescription(description) { this.gqType.description = description; return this; } clone(newTypeName) { if (!newTypeName) { throw new Error('You should provide newTypeName:string for InterfaceTypeComposer.clone()'); } const newFields = {}; this.getFieldNames().forEach(fieldName => { const fc = this.getFieldConfig(fieldName); newFields[fieldName] = _objectSpread({}, fc); }); const cloned = new InterfaceTypeComposer(new GraphQLInterfaceType({ name: newTypeName, fields: newFields }), this.schemaComposer); cloned.setDescription(this.getDescription()); return cloned; } merge(type) { if (type instanceof GraphQLInterfaceType || type instanceof GraphQLObjectType) { this.addFields(defineFieldMapToConfig(type.getFields())); } else if (type instanceof InterfaceTypeComposer || type instanceof ObjectTypeComposer) { this.addFields(type.getFields()); } else { throw new Error(`Cannot merge ${inspect(type)} with InterfaceType(${this.getTypeName()}). Provided type should be GraphQLInterfaceType, GraphQLObjectType, InterfaceTypeComposer or ObjectTypeComposer.`); } return this; } // ----------------------------------------------- // InputType methods // ----------------------------------------------- getInputType() { return this.getInputTypeComposer().getType(); } hasInputTypeComposer() { return !!this.gqType._gqcInputTypeComposer; } setInputTypeComposer(itc) { this.gqType._gqcInputTypeComposer = itc; return this; } getInputTypeComposer() { if (!this.gqType._gqcInputTypeComposer) { this.gqType._gqcInputTypeComposer = toInputObjectType(this); } return this.gqType._gqcInputTypeComposer; } getITC() { return this.getInputTypeComposer(); } removeInputTypeComposer() { this.gqType._gqcInputTypeComposer = undefined; return this; } // ----------------------------------------------- // ResolveType methods // ----------------------------------------------- getResolveType() { return this.gqType.resolveType; } setResolveType(fn) { this.gqType.resolveType = fn; return this; } hasTypeResolver(type) { const typeResolversMap = this.getTypeResolvers(); return typeResolversMap.has(type); } getTypeResolvers() { if (!this.gqType._gqcTypeResolvers) { this.gqType._gqcTypeResolvers = new Map(); } return this.gqType._gqcTypeResolvers; } getTypeResolverCheckFn(type) { const typeResolversMap = this.getTypeResolvers(); if (!typeResolversMap.has(type)) { throw new Error(`Type resolve function in interface '${this.getTypeName()}' is not defined for type ${inspect(type)}.`); } return typeResolversMap.get(type); } getTypeResolverNames() { const typeResolversMap = this.getTypeResolvers(); const names = []; typeResolversMap.forEach((resolveFn, composeType) => { if (composeType instanceof ObjectTypeComposer) { names.push(composeType.getTypeName()); } else if (composeType && composeType.name) { names.push(composeType.name); } }); return names; } getTypeResolverTypes() { const typeResolversMap = this.getTypeResolvers(); const types = []; typeResolversMap.forEach((resolveFn, composeType) => { types.push(getGraphQLType(composeType)); }); return types; } setTypeResolvers(typeResolversMap) { this._isTypeResolversValid(typeResolversMap); this.gqType._gqcTypeResolvers = typeResolversMap; // extract GraphQLObjectType from ObjectTypeComposer const fastEntries = []; for (const [composeType, checkFn] of typeResolversMap.entries()) { fastEntries.push([getGraphQLType(composeType), checkFn]); } let resolveType; const isAsyncRuntime = this._isTypeResolversAsync(typeResolversMap); if (isAsyncRuntime) { resolveType = async (value, context, info) => { for (const [gqType, checkFn] of fastEntries) { // should we run checkFn simultaniously or in serial? // Current decision is: dont SPIKE event loop - run in serial (it may be changed in future) // eslint-disable-next-line no-await-in-loop if (await checkFn(value, context, info)) return gqType; } return null; }; } else { resolveType = (value, context, info) => { for (const [gqType, checkFn] of fastEntries) { if (checkFn(value, context, info)) return gqType; } return null; }; } this.setResolveType(resolveType); return this; } _isTypeResolversValid(typeResolversMap) { if (!(typeResolversMap instanceof Map)) { throw new Error(`For interface ${this.getTypeName()} you should provide Map object for type resolvers.`); } for (const [composeType, checkFn] of typeResolversMap.entries()) { // checking composeType try { const type = getGraphQLType(composeType); if (!(type instanceof GraphQLObjectType)) throw new Error('Must be GraphQLObjectType'); } catch (e) { throw new Error(`For interface type resolver ${this.getTypeName()} you must provide GraphQLObjectType or ObjectTypeComposer, but provided ${inspect(composeType)}`); } // checking checkFn if (!isFunction(checkFn)) { throw new Error(`Interface ${this.getTypeName()} has invalid check function for type ${inspect(composeType)}`); } } return true; } // eslint-disable-next-line class-methods-use-this _isTypeResolversAsync(typeResolversMap) { let res = false; for (const [, checkFn] of typeResolversMap.entries()) { try { const r = checkFn({}, {}, {}); if (r instanceof Promise) { r.catch(() => {}); res = true; } } catch (e) {// noop } } return res; } addTypeResolver(type, checkFn) { const typeResolversMap = this.getTypeResolvers(); typeResolversMap.set(type, checkFn); this.setTypeResolvers(typeResolversMap); return this; } removeTypeResolver(type) { const typeResolversMap = this.getTypeResolvers(); typeResolversMap.delete(type); this.setTypeResolvers(typeResolversMap); return this; } // ----------------------------------------------- // Extensions methods // ----------------------------------------------- getExtensions() { if (!this.gqType._gqcExtensions) { return {}; } else { return this.gqType._gqcExtensions; } } setExtensions(extensions) { this.gqType._gqcExtensions = extensions; return this; } extendExtensions(extensions) { const current = this.getExtensions(); this.setExtensions(_objectSpread({}, current, extensions)); return this; } clearExtensions() { this.setExtensions({}); return this; } getExtension(extensionName) { const extensions = this.getExtensions(); return extensions[extensionName]; } hasExtension(extensionName) { const extensions = this.getExtensions(); return extensionName in extensions; } setExtension(extensionName, value) { this.extendExtensions({ [extensionName]: value }); return this; } removeExtension(extensionName) { const extensions = _objectSpread({}, this.getExtensions()); delete extensions[extensionName]; this.setExtensions(extensions); return this; } getFieldExtensions(fieldName) { const field = this.getField(fieldName); return field.extensions || {}; } setFieldExtensions(fieldName, extensions) { const field = this.getField(fieldName); this.setField(fieldName, _objectSpread({}, field, { extensions })); return this; } extendFieldExtensions(fieldName, extensions) { const current = this.getFieldExtensions(fieldName); this.setFieldExtensions(fieldName, _objectSpread({}, current, extensions)); return this; } clearFieldExtensions(fieldName) { this.setFieldExtensions(fieldName, {}); return this; } getFieldExtension(fieldName, extensionName) { const extensions = this.getFieldExtensions(fieldName); return extensions[extensionName]; } hasFieldExtension(fieldName, extensionName) { const extensions = this.getFieldExtensions(fieldName); return extensionName in extensions; } setFieldExtension(fieldName, extensionName, value) { this.extendFieldExtensions(fieldName, { [extensionName]: value }); return this; } removeFieldExtension(fieldName, extensionName) { const extensions = _objectSpread({}, this.getFieldExtensions(fieldName)); delete extensions[extensionName]; this.setFieldExtensions(fieldName, extensions); return this; } getFieldArgExtensions(fieldName, argName) { const ac = this.getFieldArg(fieldName, argName); return ac.extensions || {}; } setFieldArgExtensions(fieldName, argName, extensions) { const ac = this.getFieldArg(fieldName, argName); this.setFieldArg(fieldName, argName, _objectSpread({}, ac, { extensions })); return this; } extendFieldArgExtensions(fieldName, argName, extensions) { const current = this.getFieldArgExtensions(fieldName, argName); this.setFieldArgExtensions(fieldName, argName, _objectSpread({}, current, extensions)); return this; } clearFieldArgExtensions(fieldName, argName) { this.setFieldArgExtensions(fieldName, argName, {}); return this; } getFieldArgExtension(fieldName, argName, extensionName) { const extensions = this.getFieldArgExtensions(fieldName, argName); return extensions[extensionName]; } hasFieldArgExtension(fieldName, argName, extensionName) { const extensions = this.getFieldArgExtensions(fieldName, argName); return extensionName in extensions; } setFieldArgExtension(fieldName, argName, extensionName, value) { this.extendFieldArgExtensions(fieldName, argName, { [extensionName]: value }); return this; } removeFieldArgExtension(fieldName, argName, extensionName) { const extensions = _objectSpread({}, this.getFieldArgExtensions(fieldName, argName)); delete extensions[extensionName]; this.setFieldArgExtensions(fieldName, argName, extensions); return this; } // ----------------------------------------------- // Directive methods // ----------------------------------------------- getDirectives() { const directives = this.getExtension('directives'); if (Array.isArray(directives)) { return directives; } return []; } getDirectiveNames() { return this.getDirectives().map(d => d.name); } getDirectiveByName(directiveName) { const directive = this.getDirectives().find(d => d.name === directiveName); if (!directive) return undefined; return directive.args; } getDirectiveById(idx) { const directive = this.getDirectives()[idx]; if (!directive) return undefined; return directive.args; } getFieldDirectives(fieldName) { const directives = this.getFieldExtension(fieldName, 'directives'); if (Array.isArray(directives)) { return directives; } return []; } getFieldDirectiveNames(fieldName) { return this.getFieldDirectives(fieldName).map(d => d.name); } getFieldDirectiveByName(fieldName, directiveName) { const directive = this.getFieldDirectives(fieldName).find(d => d.name === directiveName); if (!directive) return undefined; return directive.args; } getFieldDirectiveById(fieldName, idx) { const directive = this.getFieldDirectives(fieldName)[idx]; if (!directive) return undefined; return directive.args; } getFieldArgDirectives(fieldName, argName) { const directives = this.getFieldArgExtension(fieldName, argName, 'directives'); if (Array.isArray(directives)) { return directives; } return []; } getFieldArgDirectiveNames(fieldName, argName) { return this.getFieldArgDirectives(fieldName, argName).map(d => d.name); } getFieldArgDirectiveByName(fieldName, argName, directiveName) { const directive = this.getFieldArgDirectives(fieldName, argName).find(d => d.name === directiveName); if (!directive) return undefined; return directive.args; } getFieldArgDirectiveById(fieldName, argName, idx) { const directive = this.getFieldArgDirectives(fieldName, argName)[idx]; if (!directive) return undefined; return directive.args; } // ----------------------------------------------- // Misc methods // ----------------------------------------------- get(path) { return typeByPath(this, path); } }