Skip to content

Commit cc69bb1

Browse files
author
Sashko Stubailo
authored
Merge pull request #529 from apollographql/directives-staging
Resolver directives continuation
2 parents f8ea3fd + 12e119d commit cc69bb1

File tree

6 files changed

+378
-23
lines changed

6 files changed

+378
-23
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
### VNEXT
44

5+
* Added support for custom directives on FIELD_DEFINITION that wrap resolvers with custom reusable logic. [Issue #212](https://github.com/apollographql/graphql-tools/issues/212) [PR #518](https://github.com/apollographql/graphql-tools/pull/518) and [PR #529](https://github.com/apollographql/graphql-tools/pull/529)
6+
7+
### v2.12.0
8+
59
* Allow passing in a string `schema` to `makeRemoteExecutableSchema` [PR #521](https://github.com/apollographql/graphql-tools/pull/521)
6-
* ...
710

811
### v2.11.0
912

src/Interfaces.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export interface IExecutableSchemaDefinition {
7676
logger?: ILogger;
7777
allowUndefinedInResolve?: boolean;
7878
resolverValidationOptions?: IResolverValidationOptions;
79+
directiveResolvers?: IDirectiveResolvers<any, any>;
7980
}
8081

8182
export type IFieldIteratorFn = (
@@ -84,6 +85,19 @@ export type IFieldIteratorFn = (
8485
fieldName: string,
8586
) => void;
8687

88+
export type NextResolverFn = () => Promise<any>;
89+
export type DirectiveResolverFn<TSource, TContext> = (
90+
next: NextResolverFn,
91+
source: TSource,
92+
args: { [argName: string]: any },
93+
context: TContext,
94+
info: GraphQLResolveInfo,
95+
) => any;
96+
97+
export interface IDirectiveResolvers<TSource, TContext> {
98+
[directiveName: string]: DirectiveResolverFn<TSource, TContext>;
99+
}
100+
87101
/* XXX on mocks, args are optional, Not sure if a bug. */
88102
export type IMockFn = GraphQLFieldResolver<any, any>;
89103
export type IMocks = { [key: string]: IMockFn };

src/schemaGenerator.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
print,
1414
Kind,
1515
DefinitionNode,
16+
DirectiveNode,
1617
defaultFieldResolver,
1718
buildASTSchema,
1819
extendSchema,
@@ -27,6 +28,7 @@ import {
2728
GraphQLInterfaceType,
2829
GraphQLFieldMap,
2930
} from 'graphql';
31+
import { getArgumentValues } from 'graphql/execution/values';
3032
import {
3133
IExecutableSchemaDefinition,
3234
ILogger,
@@ -38,6 +40,7 @@ import {
3840
IConnector,
3941
IConnectorCls,
4042
IResolverValidationOptions,
43+
IDirectiveResolvers,
4144
} from './Interfaces';
4245

4346
import { deprecated } from 'deprecated-decorator';
@@ -62,6 +65,7 @@ function _generateSchema(
6265
// TODO: rename to allowUndefinedInResolve to be consistent
6366
allowUndefinedInResolve: boolean,
6467
resolverValidationOptions: IResolverValidationOptions,
68+
directiveResolvers: IDirectiveResolvers<any, any>,
6569
) {
6670
if (typeof resolverValidationOptions !== 'object') {
6771
throw new SchemaError(
@@ -95,6 +99,10 @@ function _generateSchema(
9599
addErrorLoggingToSchema(schema, logger);
96100
}
97101

102+
if (directiveResolvers) {
103+
attachDirectiveResolvers(schema, directiveResolvers);
104+
}
105+
98106
return schema;
99107
}
100108

@@ -105,13 +113,15 @@ function makeExecutableSchema({
105113
logger,
106114
allowUndefinedInResolve = true,
107115
resolverValidationOptions = {},
116+
directiveResolvers = null,
108117
}: IExecutableSchemaDefinition) {
109118
const jsSchema = _generateSchema(
110119
typeDefs,
111120
resolvers,
112121
logger,
113122
allowUndefinedInResolve,
114123
resolverValidationOptions,
124+
directiveResolvers,
115125
);
116126
if (typeof resolvers['__schema'] === 'function') {
117127
// TODO a bit of a hack now, better rewrite generateSchema to attach it there.
@@ -378,11 +388,12 @@ function addResolveFunctionsToSchema(
378388
// is inside NPM
379389
if (!(type as any).isValidValue(fieldName)) {
380390
throw new SchemaError(
381-
`${typeName}.${fieldName} was defined in resolvers, but enum is not in schema`,
391+
`${typeName}.${fieldName} was defined in resolvers, but enum is not in schema`,
382392
);
383393
}
384394

385-
type.getValue(fieldName)['value'] = resolveFunctions[typeName][fieldName];
395+
type.getValue(fieldName)['value'] =
396+
resolveFunctions[typeName][fieldName];
386397
return;
387398
}
388399

@@ -637,6 +648,62 @@ function runAtMostOncePerRequest(
637648
};
638649
}
639650

651+
function attachDirectiveResolvers(
652+
schema: GraphQLSchema,
653+
directiveResolvers: IDirectiveResolvers<any, any>,
654+
) {
655+
if (typeof directiveResolvers !== 'object') {
656+
throw new Error(
657+
`Expected directiveResolvers to be of type object, got ${typeof directiveResolvers}`,
658+
);
659+
}
660+
if (Array.isArray(directiveResolvers)) {
661+
throw new Error(
662+
'Expected directiveResolvers to be of type object, got Array',
663+
);
664+
}
665+
forEachField(schema, (field: GraphQLField<any, any>) => {
666+
const directives = field.astNode.directives;
667+
directives.forEach((directive: DirectiveNode) => {
668+
const directiveName = directive.name.value;
669+
const resolver = directiveResolvers[directiveName];
670+
671+
if (resolver) {
672+
const originalResolver = field.resolve || defaultFieldResolver;
673+
const Directive = schema.getDirective(directiveName);
674+
if (typeof Directive === 'undefined') {
675+
throw new Error(
676+
`Directive @${directiveName} is undefined. ` +
677+
'Please define in schema before using',
678+
);
679+
}
680+
const directiveArgs = getArgumentValues(Directive, directive);
681+
682+
field.resolve = (...args: any[]) => {
683+
const [source, , context, info] = args;
684+
return resolver(
685+
() => {
686+
try {
687+
const promise = originalResolver.call(field, ...args);
688+
if (promise instanceof Promise) {
689+
return promise;
690+
}
691+
return Promise.resolve(promise);
692+
} catch (error) {
693+
return Promise.reject(error);
694+
}
695+
},
696+
source,
697+
directiveArgs,
698+
context,
699+
info,
700+
);
701+
};
702+
}
703+
});
704+
});
705+
}
706+
640707
export {
641708
makeExecutableSchema,
642709
SchemaError,
@@ -650,4 +717,5 @@ export {
650717
addSchemaLevelResolveFunction,
651718
attachConnectorsToContext,
652719
concatenateTypeDefs,
720+
attachDirectiveResolvers,
653721
};

src/stitching/TypeRegistry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default class TypeRegistry {
2727
this.schemaByField = {
2828
query: {},
2929
mutation: {},
30-
subscription: {}
30+
subscription: {},
3131
};
3232
this.fragmentReplacements = {};
3333
}

src/stitching/linkToFetcher.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,11 @@ function execute(
4949
link: ApolloLink,
5050
operation: GraphQLRequest,
5151
): Observable<FetchResult> {
52-
return (
53-
link.request(
54-
createOperation(
55-
operation.context,
56-
transformOperation(validateOperation(operation)),
57-
),
58-
)
52+
return link.request(
53+
createOperation(
54+
operation.context,
55+
transformOperation(validateOperation(operation)),
56+
),
5957
);
6058
}
6159

0 commit comments

Comments
 (0)