Skip to content

Commit 337fd4f

Browse files
[typescript-resolvers] Add directiveContextExtender option (#7506)
* Add directivedContextExtender config * Create brown-rockets-tan.md * add multiple tests * update docs Co-authored-by: Charly POLY <1252066+charlypoly@users.noreply.github.com>
1 parent 5ce7c80 commit 337fd4f

File tree

4 files changed

+222
-8
lines changed

4 files changed

+222
-8
lines changed

‎.changeset/brown-rockets-tan.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphql-codegen/typescript-resolvers': patch
3+
'@graphql-codegen/visitor-plugin-common': patch
4+
---
5+
6+
WP: [typescript-resolvers] Add directiveContextTypes option

‎packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import { getRootTypeNames } from '@graphql-tools/utils';
5454
export interface ParsedResolversConfig extends ParsedConfig {
5555
contextType: ParsedMapper;
5656
fieldContextTypes: Array<string>;
57+
directiveContextTypes: Array<string>;
5758
rootValueType: ParsedMapper;
5859
mappers: {
5960
[typeName: string]: ParsedMapper;
@@ -152,6 +153,25 @@ export interface RawResolversConfig extends RawConfig {
152153
* rootValueType: ./my-types#MyRootValue
153154
* ```
154155
*/
156+
directiveContextTypes?: Array<string>;
157+
/**
158+
* @description Use this to set a custom type for a specific field `context` decorated by a directive.
159+
* It will only affect the targeted resolvers.
160+
* You can either use `Field.Path#ContextTypeName` or `Field.Path#ExternalFileName#ContextTypeName`
161+
*
162+
* ContextTypeName should by a generic Type that take the context or field context type as only type parameter.
163+
*
164+
* @exampleMarkdown
165+
* ## Directive Context Extender
166+
*
167+
* ```yml
168+
* plugins
169+
* config:
170+
* directiveContextTypes:
171+
* - myCustomDirectiveName#./my-file#CustomContextExtender
172+
* ```
173+
*
174+
*/
155175
rootValueType?: string;
156176
/**
157177
* @description Adds a suffix to the imported names to prevent name clashes.
@@ -375,6 +395,7 @@ export class BaseResolversVisitor<
375395
protected _hasScalars = false;
376396
protected _hasFederation = false;
377397
protected _fieldContextTypeMap: FieldContextTypeMap;
398+
protected _directiveContextTypesMap: FieldContextTypeMap;
378399
private _directiveResolverMappings: Record<string, string>;
379400
private _shouldMapType: { [typeName: string]: boolean } = {};
380401

@@ -398,6 +419,7 @@ export class BaseResolversVisitor<
398419
onlyResolveTypeForInterfaces: getConfigValue(rawConfig.onlyResolveTypeForInterfaces, false),
399420
contextType: parseMapper(rawConfig.contextType || 'any', 'ContextType'),
400421
fieldContextTypes: getConfigValue(rawConfig.fieldContextTypes, []),
422+
directiveContextTypes: getConfigValue(rawConfig.directiveContextTypes, []),
401423
resolverTypeSuffix: getConfigValue(rawConfig.resolverTypeSuffix, 'Resolvers'),
402424
allResolversTypeName: getConfigValue(rawConfig.allResolversTypeName, 'Resolvers'),
403425
rootValueType: parseMapper(rawConfig.rootValueType || '{}', 'RootValueType'),
@@ -432,6 +454,7 @@ export class BaseResolversVisitor<
432454
namedType => !isEnumType(namedType)
433455
);
434456
this._fieldContextTypeMap = this.createFieldContextTypeMap();
457+
this._directiveContextTypesMap = this.createDirectivedContextType();
435458
this._directiveResolverMappings = rawConfig.directiveResolverMappings ?? {};
436459
}
437460

@@ -696,6 +719,17 @@ export class BaseResolversVisitor<
696719
return { ...prev, [path]: parseMapper(contextType) };
697720
}, {});
698721
}
722+
protected createDirectivedContextType(): FieldContextTypeMap {
723+
return this.config.directiveContextTypes.reduce<FieldContextTypeMap>((prev, fieldContextType) => {
724+
const items = fieldContextType.split('#');
725+
if (items.length === 3) {
726+
const [path, source, contextTypeName] = items;
727+
return { ...prev, [path]: parseMapper(`${source}#${contextTypeName}`) };
728+
}
729+
const [path, contextType] = items;
730+
return { ...prev, [path]: parseMapper(contextType) };
731+
}, {});
732+
}
699733

700734
public buildResolversTypes(): string {
701735
const declarationKind = 'type';
@@ -795,6 +829,12 @@ export class BaseResolversVisitor<
795829
}
796830
});
797831

832+
Object.values(this._directiveContextTypesMap).forEach(parsedMapper => {
833+
if (parsedMapper.isExternal) {
834+
addMapper(parsedMapper.source, parsedMapper.import, parsedMapper.default);
835+
}
836+
});
837+
798838
return Object.keys(groupedMappers)
799839
.map(source => buildMapperImport(source, groupedMappers[source], this.config.useTypeImports))
800840
.filter(Boolean);
@@ -938,6 +978,8 @@ export class BaseResolversVisitor<
938978
return null;
939979
}
940980

981+
const contextType = this.getContextType(parentName, node);
982+
941983
const typeToUse = this.getTypeToUse(realType);
942984
const mappedType = this._variablesTransformer.wrapAstTypeWithModifiers(typeToUse, original.type);
943985
const subscriptionType = this._schema.getSubscriptionType();
@@ -996,14 +1038,7 @@ export class BaseResolversVisitor<
9961038
name: node.name as any,
9971039
modifier: avoidOptionals ? '' : '?',
9981040
type: resolverType,
999-
genericTypes: [
1000-
mappedTypeKey,
1001-
parentTypeSignature,
1002-
this._fieldContextTypeMap[`${parentName}.${node.name}`]
1003-
? this._fieldContextTypeMap[`${parentName}.${node.name}`].type
1004-
: 'ContextType',
1005-
argsType,
1006-
].filter(f => f),
1041+
genericTypes: [mappedTypeKey, parentTypeSignature, contextType, argsType].filter(f => f),
10071042
};
10081043

10091044
if (this._federation.isResolveReferenceField(node)) {
@@ -1023,6 +1058,26 @@ export class BaseResolversVisitor<
10231058
};
10241059
}
10251060

1061+
private getFieldContextType(parentName: string, node: FieldDefinitionNode): string {
1062+
if (this._fieldContextTypeMap[`${parentName}.${node.name}`]) {
1063+
return this._fieldContextTypeMap[`${parentName}.${node.name}`].type;
1064+
}
1065+
return 'ContextType';
1066+
}
1067+
1068+
private getContextType(parentName: string, node: FieldDefinitionNode): string {
1069+
let contextType = this.getFieldContextType(parentName, node);
1070+
1071+
for (const directive of node.directives) {
1072+
const name = directive.name as unknown as string;
1073+
const directiveMap = this._directiveContextTypesMap[name];
1074+
if (directiveMap) {
1075+
contextType = `${directiveMap.type}<${contextType}>`;
1076+
}
1077+
}
1078+
return contextType;
1079+
}
1080+
10261081
protected applyRequireFields(argsType: string, fields: InputValueDefinitionNode[]): string {
10271082
this._globalDeclarations.add(REQUIRE_FIELDS_TYPE);
10281083
return `RequireFields<${argsType}, ${fields.map(f => `'${f.name.value}'`).join(' | ')}>`;

‎packages/plugins/typescript/resolvers/tests/ts-resolvers.spec.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,80 @@ __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
962962
await validate(result);
963963
});
964964

965+
it('Should allow to override context with mapped context type', async () => {
966+
const result = (await plugin(
967+
schema,
968+
[],
969+
{
970+
contextType: './my-file#MyCustomCtx',
971+
},
972+
{ outputFile: '' }
973+
)) as Types.ComplexPluginOutput;
974+
975+
expect(result.prepend).toContain(`import { MyCustomCtx } from './my-file';`);
976+
977+
expect(result.content).toBeSimilarStringTo(`
978+
export type MyDirectiveDirectiveArgs = {
979+
arg: Scalars['Int'];
980+
arg2: Scalars['String'];
981+
arg3: Scalars['Boolean'];
982+
};
983+
`);
984+
985+
expect(result.content).toBeSimilarStringTo(`
986+
export type MyDirectiveDirectiveResolver<Result, Parent, ContextType = MyCustomCtx, Args = MyDirectiveDirectiveArgs> = DirectiveResolverFn<Result, Parent, ContextType, Args>;`);
987+
988+
expect(result.content).toBeSimilarStringTo(`
989+
export type MyOtherTypeResolvers<ContextType = MyCustomCtx, ParentType extends ResolversParentTypes['MyOtherType'] = ResolversParentTypes['MyOtherType']> = {
990+
bar?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
991+
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
992+
};
993+
`);
994+
995+
expect(result.content).toBeSimilarStringTo(`
996+
export type MyTypeResolvers<ContextType = MyCustomCtx, ParentType extends ResolversParentTypes['MyType'] = ResolversParentTypes['MyType']> = {
997+
foo?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
998+
otherType?: Resolver<Maybe<ResolversTypes['MyOtherType']>, ParentType, ContextType>;
999+
withArgs?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType, RequireFields<MyTypeWithArgsArgs, 'arg2'>>;
1000+
unionChild?: Resolver<Maybe<ResolversTypes['ChildUnion']>, ParentType, ContextType>;
1001+
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
1002+
};
1003+
`);
1004+
1005+
expect(result.content).toBeSimilarStringTo(`
1006+
export type MyUnionResolvers<ContextType = MyCustomCtx, ParentType extends ResolversParentTypes['MyUnion'] = ResolversParentTypes['MyUnion']> = {
1007+
__resolveType: TypeResolveFn<'MyType' | 'MyOtherType', ParentType, ContextType>;
1008+
};
1009+
`);
1010+
1011+
expect(result.content).toBeSimilarStringTo(`
1012+
export type NodeResolvers<ContextType = MyCustomCtx, ParentType extends ResolversParentTypes['Node'] = ResolversParentTypes['Node']> = {
1013+
__resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>;
1014+
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
1015+
};
1016+
`);
1017+
1018+
expect(result.content).toBeSimilarStringTo(`
1019+
export type QueryResolvers<ContextType = MyCustomCtx, ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query']> = {
1020+
something?: Resolver<ResolversTypes['MyType'], ParentType, ContextType>;
1021+
};
1022+
`);
1023+
1024+
expect(result.content).toBeSimilarStringTo(`
1025+
export type SomeNodeResolvers<ContextType = MyCustomCtx, ParentType extends ResolversParentTypes['SomeNode'] = ResolversParentTypes['SomeNode']> = {
1026+
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
1027+
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
1028+
};
1029+
`);
1030+
1031+
expect(result.content).toBeSimilarStringTo(`
1032+
export type SubscriptionResolvers<ContextType = MyCustomCtx, ParentType extends ResolversParentTypes['Subscription'] = ResolversParentTypes['Subscription']> = {
1033+
somethingChanged?: SubscriptionResolver<Maybe<ResolversTypes['MyOtherType']>, "somethingChanged", ParentType, ContextType>;
1034+
};
1035+
`);
1036+
1037+
await validate(result);
1038+
});
9651039
it('Should allow to override context with mapped context type as default export', async () => {
9661040
const result = (await plugin(
9671041
schema,
@@ -1145,6 +1219,77 @@ __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
11451219
`);
11461220
});
11471221

1222+
it('should generate named custom field level context type for field with directive', async () => {
1223+
const result = (await plugin(
1224+
schema,
1225+
[],
1226+
{
1227+
directiveContextTypes: ['authenticated#./my-file#AuthenticatedContext'],
1228+
},
1229+
{ outputFile: '' }
1230+
)) as Types.ComplexPluginOutput;
1231+
1232+
expect(result.prepend).toContain(`import { AuthenticatedContext } from './my-file';`);
1233+
1234+
expect(result.content).toBeSimilarStringTo(`
1235+
export type MyTypeResolvers<ContextType = any, ParentType extends ResolversParentTypes['MyType'] = ResolversParentTypes['MyType']> = {
1236+
foo?: Resolver<ResolversTypes['String'], ParentType, AuthenticatedContext<ContextType>>;
1237+
otherType?: Resolver<Maybe<ResolversTypes['MyOtherType']>, ParentType, ContextType>;
1238+
withArgs?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType, RequireFields<MyTypeWithArgsArgs, 'arg2'>>;
1239+
unionChild?: Resolver<Maybe<ResolversTypes['ChildUnion']>, ParentType, ContextType>;
1240+
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
1241+
};
1242+
`);
1243+
});
1244+
1245+
it('should generate named custom field level context type for field with directive and context type', async () => {
1246+
const result = (await plugin(
1247+
schema,
1248+
[],
1249+
{
1250+
directiveContextTypes: ['authenticated#./my-file#AuthenticatedContext'],
1251+
contextType: './my-file#MyCustomCtx',
1252+
},
1253+
{ outputFile: '' }
1254+
)) as Types.ComplexPluginOutput;
1255+
1256+
expect(result.prepend).toContain(`import { MyCustomCtx, AuthenticatedContext } from './my-file';`);
1257+
1258+
expect(result.content).toBeSimilarStringTo(`
1259+
export type MyTypeResolvers<ContextType = MyCustomCtx, ParentType extends ResolversParentTypes['MyType'] = ResolversParentTypes['MyType']> = {
1260+
foo?: Resolver<ResolversTypes['String'], ParentType, AuthenticatedContext<ContextType>>;
1261+
otherType?: Resolver<Maybe<ResolversTypes['MyOtherType']>, ParentType, ContextType>;
1262+
withArgs?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType, RequireFields<MyTypeWithArgsArgs, 'arg2'>>;
1263+
unionChild?: Resolver<Maybe<ResolversTypes['ChildUnion']>, ParentType, ContextType>;
1264+
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
1265+
};
1266+
`);
1267+
});
1268+
1269+
it('should generate named custom field level context type for field with directive and field context type', async () => {
1270+
const result = (await plugin(
1271+
schema,
1272+
[],
1273+
{
1274+
directiveContextTypes: ['authenticated#./my-file#AuthenticatedContext'],
1275+
fieldContextTypes: ['MyType.foo#./my-file#ContextTypeOne'],
1276+
},
1277+
{ outputFile: '' }
1278+
)) as Types.ComplexPluginOutput;
1279+
1280+
expect(result.prepend).toContain(`import { ContextTypeOne, AuthenticatedContext } from './my-file';`);
1281+
1282+
expect(result.content).toBeSimilarStringTo(`
1283+
export type MyTypeResolvers<ContextType = any, ParentType extends ResolversParentTypes['MyType'] = ResolversParentTypes['MyType']> = {
1284+
foo?: Resolver<ResolversTypes['String'], ParentType, AuthenticatedContext<ContextTypeOne>>;
1285+
otherType?: Resolver<Maybe<ResolversTypes['MyOtherType']>, ParentType, ContextType>;
1286+
withArgs?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType, RequireFields<MyTypeWithArgsArgs, 'arg2'>>;
1287+
unionChild?: Resolver<Maybe<ResolversTypes['ChildUnion']>, ParentType, ContextType>;
1288+
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
1289+
};
1290+
`);
1291+
});
1292+
11481293
it('Should generate the correct imports when schema has scalars', async () => {
11491294
const testSchema = buildSchema(`scalar MyScalar`);
11501295
const result = await plugin(testSchema, [], {}, { outputFile: '' });

‎website/public/config.schema.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1433,6 +1433,10 @@
14331433
"$ref": "#/definitions/Array_1",
14341434
"description": "Use this to set a custom type for a specific field `context`.\nIt will only affect the targeted resolvers.\nYou can either use `Field.Path#ContextTypeName` or `Field.Path#ExternalFileName#ContextTypeName`"
14351435
},
1436+
"directiveContextTypes": {
1437+
"$ref": "#/definitions/Array_1",
1438+
"description": "Use this to set a custom type for a field `context` decoreted by a specific directive.\nIt will only affect the targeted resolvers.\nYou can either use `Field.Path#ContextTypeName` or `Field.Path#ExternalFileName#ContextTypeName`"
1439+
},
14361440
"rootValueType": {
14371441
"description": "Use this configuration to set a custom type for the `rootValue`, and it will\naffect resolvers of all root types (Query, Mutation and Subscription), without the need to override it using generics each time.\nIf you wish to use an external type and import it from another file, you can use `add` plugin\nand add the required `import` statement, or you can use both `module#type` or `module#namespace#type` syntax.",
14381442
"type": "string"
@@ -3035,6 +3039,10 @@
30353039
"$ref": "#/definitions/Array_1",
30363040
"description": "Use this to set a custom type for a specific field `context`.\nIt will only affect the targeted resolvers.\nYou can either use `Field.Path#ContextTypeName` or `Field.Path#ExternalFileName#ContextTypeName`"
30373041
},
3042+
"directiveContextTypes": {
3043+
"$ref": "#/definitions/Array_1",
3044+
"description": "Use this to set a custom type for a field `context` decoreted by a specific directive.\nIt will only affect the targeted resolvers.\nYou can either use `Field.Path#ContextTypeName` or `Field.Path#ExternalFileName#ContextTypeName`"
3045+
},
30383046
"rootValueType": {
30393047
"description": "Use this configuration to set a custom type for the `rootValue`, and it will\naffect resolvers of all root types (Query, Mutation and Subscription), without the need to override it using generics each time.\nIf you wish to use an external type and import it from another file, you can use `add` plugin\nand add the required `import` statement, or you can use both `module#type` or `module#namespace#type` syntax.",
30403048
"type": "string"

0 commit comments

Comments
 (0)