diff --git a/packages/plugins/flow/resolvers/src/flow-util-types.ts b/packages/plugins/flow/resolvers/src/flow-util-types.ts new file mode 100644 index 00000000000..51d80a4633a --- /dev/null +++ b/packages/plugins/flow/resolvers/src/flow-util-types.ts @@ -0,0 +1 @@ +export const FLOW_REQUIRE_FIELDS_TYPE = `export type $RequireFields = $Diff & $ObjMapi(k: Key) => $NonMaybeType<$ElementType>>;`; diff --git a/packages/plugins/flow/resolvers/src/index.ts b/packages/plugins/flow/resolvers/src/index.ts index a7743f8a0f2..489c13c5b54 100644 --- a/packages/plugins/flow/resolvers/src/index.ts +++ b/packages/plugins/flow/resolvers/src/index.ts @@ -83,7 +83,7 @@ ${resolverTypeWrapper} } return { - prepend: [gqlImports, ...mappersImports], + prepend: [gqlImports, ...mappersImports, ...visitor.globalDeclarations], content: [header, resolversTypeMapping, resolversParentTypeMapping, ...visitorResult.definitions.filter(d => typeof d === 'string'), getRootResolver(), getAllDirectiveResolvers()].join('\n'), }; }; diff --git a/packages/plugins/flow/resolvers/src/visitor.ts b/packages/plugins/flow/resolvers/src/visitor.ts index 93a543b1100..473c84fa478 100644 --- a/packages/plugins/flow/resolvers/src/visitor.ts +++ b/packages/plugins/flow/resolvers/src/visitor.ts @@ -1,8 +1,9 @@ import { FlowResolversPluginConfig } from './index'; -import { ListTypeNode, NamedTypeNode, NonNullTypeNode, GraphQLSchema, ScalarTypeDefinitionNode } from 'graphql'; +import { ListTypeNode, NamedTypeNode, NonNullTypeNode, GraphQLSchema, ScalarTypeDefinitionNode, InputValueDefinitionNode } from 'graphql'; import * as autoBind from 'auto-bind'; import { indent, ParsedResolversConfig, BaseResolversVisitor, DeclarationBlock } from '@graphql-codegen/visitor-plugin-common'; import { FlowOperationVariablesToObject } from '@graphql-codegen/flow'; +import { FLOW_REQUIRE_FIELDS_TYPE } from './flow-util-types'; export interface ParsedFlorResolversConfig extends ParsedResolversConfig {} @@ -17,6 +18,11 @@ export class FlowResolversVisitor extends BaseResolversVisitor`; } + protected applyRequireFields(argsType: string, fields: InputValueDefinitionNode[]): string { + this._globalDeclarations.add(FLOW_REQUIRE_FIELDS_TYPE); + return `$RequireFields<${argsType}, { ${fields.map(f => `${f.name.value}: *`).join(', ')} }>`; + } + protected buildMapperImport(source: string, types: { identifier: string; asDefault?: boolean }[]): string { if (types[0] && types[0].asDefault) { return `import type ${types[0].identifier} from '${source}';`; diff --git a/packages/plugins/flow/resolvers/tests/flow-resolvers.spec.ts b/packages/plugins/flow/resolvers/tests/flow-resolvers.spec.ts index 354c528ebe0..e8e82f96401 100644 --- a/packages/plugins/flow/resolvers/tests/flow-resolvers.spec.ts +++ b/packages/plugins/flow/resolvers/tests/flow-resolvers.spec.ts @@ -11,8 +11,22 @@ describe('Flow Resolvers Plugin', () => { expect(result).toMatchSnapshot(); }); + it('Default values of args and compatibility with typescript plugin', async () => { + const testSchema = buildSchema(/* GraphQL */ ` + type Query { + something(arg: String = "default_value"): String + } + `); + + const config: any = { noSchemaStitching: true }; + const result = (await plugin(testSchema, [], config, { outputFile: '' })) as Types.ComplexPluginOutput; + + expect(result.prepend).toContain(`export type $RequireFields = $Diff & $ObjMapi(k: Key) => $NonMaybeType<$ElementType>>;`); + expect(result.content).toContain(`something?: Resolver, ParentType, ContextType, $RequireFields>,`); + }); + it('Should generate ResolversParentTypes', () => { - const result = plugin(schema, [], {}, { outputFile: '' }); + const result = plugin(schema, [], {}, { outputFile: '' }) as Types.ComplexPluginOutput; expect(result.content).toBeSimilarStringTo(` /** Mapping between all available schema types and the resolvers parents */ @@ -72,7 +86,7 @@ describe('Flow Resolvers Plugin', () => { { rootValueType: 'MyRoot', asyncResolverTypes: true, - }, + } as any, { outputFile: 'graphql.ts' } )) as Types.ComplexPluginOutput; diff --git a/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts index a16678fcc63..e66d3740901 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts @@ -2,7 +2,7 @@ import { ParsedConfig, RawConfig, BaseVisitor } from './base-visitor'; import * as autoBind from 'auto-bind'; import { DEFAULT_SCALARS } from './scalars'; import { ScalarsMap, EnumValuesMap, ParsedEnumValuesMap } from './types'; -import { DeclarationBlock, DeclarationBlockConfig, indent, getBaseTypeNode, buildScalars, getConfigValue, getBaseType, getRootTypeNames, stripMapperTypeInterpolation } from './utils'; +import { DeclarationBlock, DeclarationBlockConfig, indent, getBaseTypeNode, buildScalars, getConfigValue, getBaseType, getRootTypeNames, stripMapperTypeInterpolation, OMIT_TYPE, REQUIRE_FIELDS_TYPE } from './utils'; import { NameNode, ListTypeNode, @@ -206,6 +206,7 @@ export class BaseResolversVisitor = new Set(); constructor(rawConfig: TRawConfig, additionalConfig: TPluginConfig, private _schema: GraphQLSchema, defaultScalars: ScalarsMap = DEFAULT_SCALARS) { super( @@ -377,6 +378,7 @@ export class BaseResolversVisitor `'${f.fieldName}'`).join(' | ')}> & { ${relevantFields.map(f => `${f.fieldName}${f.addOptionalSign ? '?' : ''}: ${f.replaceWithType}`).join(', ')} }`; } @@ -455,6 +457,10 @@ export class BaseResolversVisitor !this._usedMappers[name]); } + public get globalDeclarations(): string[] { + return Array.from(this._globalDeclarations); + } + public get mappersImports(): string[] { const groupedMappers: { [sourceFile: string]: { identifier: string; asDefault?: boolean }[] } = {}; @@ -632,31 +638,41 @@ export type IDirectiveResolvers${contextType} = ${name};` const hasArguments = node.arguments && node.arguments.length > 0; return (parentName: string) => { - const original = parent[key]; + const original: FieldDefinitionNode = parent[key]; const baseType = getBaseTypeNode(original.type); const realType = baseType.name.value; const typeToUse = this.getTypeToUse(realType); const mappedType = this._variablesTransfomer.wrapAstTypeWithModifiers(typeToUse, original.type); const subscriptionType = this._schema.getSubscriptionType(); const isSubscriptionType = subscriptionType && subscriptionType.name === parentName; + let argsType = hasArguments + ? `${this.convertName(parentName, { + useTypesPrefix: true, + }) + + (this.config.addUnderscoreToArgsType ? '_' : '') + + this.convertName(node.name, { + useTypesPrefix: false, + }) + + 'Args'}` + : null; + + if (argsType !== null) { + const argsToForceRequire = original.arguments.filter(arg => !!arg.defaultValue); + + if (argsToForceRequire.length > 0) { + argsType = this.applyRequireFields(argsType, argsToForceRequire); + } + } - return indent( - `${node.name}${this.config.avoidOptionals ? '' : '?'}: ${isSubscriptionType ? 'SubscriptionResolver' : 'Resolver'}<${mappedType}, ParentType, ContextType${ - hasArguments - ? `, ${this.convertName(parentName, { - useTypesPrefix: true, - }) + - (this.config.addUnderscoreToArgsType ? '_' : '') + - this.convertName(node.name, { - useTypesPrefix: false, - }) + - 'Args'}` - : '' - }>,` - ); + return indent(`${node.name}${this.config.avoidOptionals ? '' : '?'}: ${isSubscriptionType ? 'SubscriptionResolver' : 'Resolver'}<${mappedType}, ParentType, ContextType${argsType ? `, ${argsType}` : ''}>,`); }; } + protected applyRequireFields(argsType: string, fields: InputValueDefinitionNode[]): string { + this._globalDeclarations.add(REQUIRE_FIELDS_TYPE); + return `RequireFields<${argsType}, ${fields.map(f => `'${f.name.value}'`).join(', ')}>`; + } + ObjectTypeDefinition(node: ObjectTypeDefinitionNode) { const name = this.convertName(node, { suffix: 'Resolvers', diff --git a/packages/plugins/other/visitor-plugin-common/src/utils.ts b/packages/plugins/other/visitor-plugin-common/src/utils.ts index e7277913275..4eae242b916 100644 --- a/packages/plugins/other/visitor-plugin-common/src/utils.ts +++ b/packages/plugins/other/visitor-plugin-common/src/utils.ts @@ -273,3 +273,4 @@ export function stripMapperTypeInterpolation(identifier: string): string { } export const OMIT_TYPE = 'export type Omit = Pick>;'; +export const REQUIRE_FIELDS_TYPE = `export type RequireFields = { [X in Exclude]?: T[X] } & { [P in K]-?: NonNullable };`; diff --git a/packages/plugins/typescript/resolvers/src/index.ts b/packages/plugins/typescript/resolvers/src/index.ts index 72dc76518f7..8cce3053eac 100644 --- a/packages/plugins/typescript/resolvers/src/index.ts +++ b/packages/plugins/typescript/resolvers/src/index.ts @@ -1,4 +1,4 @@ -import { RawResolversConfig, OMIT_TYPE } from '@graphql-codegen/visitor-plugin-common'; +import { RawResolversConfig } from '@graphql-codegen/visitor-plugin-common'; import { Types, PluginFunction } from '@graphql-codegen/plugin-helpers'; import { isScalarType, parse, printSchema, visit, GraphQLSchema } from 'graphql'; import { TypeScriptResolversVisitor } from './visitor'; @@ -172,7 +172,7 @@ export type DirectiveResolverFn typeof d === 'string'), getRootResolver(), getAllDirectiveResolvers()].join('\n'), }; }; diff --git a/packages/plugins/typescript/resolvers/src/visitor.ts b/packages/plugins/typescript/resolvers/src/visitor.ts index 4cd92efabcb..11695f93242 100644 --- a/packages/plugins/typescript/resolvers/src/visitor.ts +++ b/packages/plugins/typescript/resolvers/src/visitor.ts @@ -1,7 +1,7 @@ import { TypeScriptResolversPluginConfig } from './index'; import { ListTypeNode, NamedTypeNode, NonNullTypeNode, GraphQLSchema } from 'graphql'; import * as autoBind from 'auto-bind'; -import { ParsedResolversConfig, BaseResolversVisitor } from '@graphql-codegen/visitor-plugin-common'; +import { ParsedResolversConfig, BaseResolversVisitor, getConfigValue } from '@graphql-codegen/visitor-plugin-common'; import { TypeScriptOperationVariablesToObject } from '@graphql-codegen/typescript'; export interface ParsedTypeScriptResolversConfig extends ParsedResolversConfig { @@ -15,10 +15,10 @@ export class TypeScriptResolversVisitor extends BaseResolversVisitor { await validate(result); }); + it('Default values of args and compatibility with typescript plugin', async () => { + const testSchema = buildSchema(/* GraphQL */ ` + type Query { + something(arg: String = "default_value"): String + } + `); + + const config: any = { noSchemaStitching: true }; + const result = (await plugin(testSchema, [], config, { outputFile: '' })) as Types.ComplexPluginOutput; + const mergedOutputs = mergeOutputs([ + result, + { + content: ` + const resolvers: QueryResolvers = { + something: (root, args, context, info) => { + return args.arg; // This should work becuase "args.arg" is now forced + } + };`, + }, + ]); + + expect(mergedOutputs).toContain(`export type RequireFields`); + expect(mergedOutputs).toContain(`something?: Resolver, ParentType, ContextType, RequireFields>,`); + validate(mergedOutputs); + }); + it('Test for enum usage in resolvers (to verify compatibility with enumValues)', async () => { const testSchema = buildSchema(/* GraphQL */ ` type Query { @@ -1072,7 +1098,7 @@ describe('TypeScript Resolvers Plugin', () => { { rootValueType: 'MyRoot', asyncResolverTypes: true, - }, + } as any, { outputFile: 'graphql.ts' } )) as Types.ComplexPluginOutput;