Skip to content

Resolver directives continuation #529

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Dec 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

### VNEXT

* 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)

### v2.12.0

* Allow passing in a string `schema` to `makeRemoteExecutableSchema` [PR #521](https://github.com/apollographql/graphql-tools/pull/521)
* ...

### v2.11.0

Expand Down
14 changes: 14 additions & 0 deletions src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface IExecutableSchemaDefinition {
logger?: ILogger;
allowUndefinedInResolve?: boolean;
resolverValidationOptions?: IResolverValidationOptions;
directiveResolvers?: IDirectiveResolvers<any, any>;
}

export type IFieldIteratorFn = (
Expand All @@ -84,6 +85,19 @@ export type IFieldIteratorFn = (
fieldName: string,
) => void;

export type NextResolverFn = () => Promise<any>;
export type DirectiveResolverFn<TSource, TContext> = (
next: NextResolverFn,
source: TSource,
args: { [argName: string]: any },
context: TContext,
info: GraphQLResolveInfo,
) => any;

export interface IDirectiveResolvers<TSource, TContext> {
[directiveName: string]: DirectiveResolverFn<TSource, TContext>;
}

/* XXX on mocks, args are optional, Not sure if a bug. */
export type IMockFn = GraphQLFieldResolver<any, any>;
export type IMocks = { [key: string]: IMockFn };
Expand Down
72 changes: 70 additions & 2 deletions src/schemaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
print,
Kind,
DefinitionNode,
DirectiveNode,
defaultFieldResolver,
buildASTSchema,
extendSchema,
Expand All @@ -27,6 +28,7 @@ import {
GraphQLInterfaceType,
GraphQLFieldMap,
} from 'graphql';
import { getArgumentValues } from 'graphql/execution/values';
import {
IExecutableSchemaDefinition,
ILogger,
Expand All @@ -38,6 +40,7 @@ import {
IConnector,
IConnectorCls,
IResolverValidationOptions,
IDirectiveResolvers,
} from './Interfaces';

import { deprecated } from 'deprecated-decorator';
Expand All @@ -62,6 +65,7 @@ function _generateSchema(
// TODO: rename to allowUndefinedInResolve to be consistent
allowUndefinedInResolve: boolean,
resolverValidationOptions: IResolverValidationOptions,
directiveResolvers: IDirectiveResolvers<any, any>,
) {
if (typeof resolverValidationOptions !== 'object') {
throw new SchemaError(
Expand Down Expand Up @@ -95,6 +99,10 @@ function _generateSchema(
addErrorLoggingToSchema(schema, logger);
}

if (directiveResolvers) {
attachDirectiveResolvers(schema, directiveResolvers);
}

return schema;
}

Expand All @@ -105,13 +113,15 @@ function makeExecutableSchema({
logger,
allowUndefinedInResolve = true,
resolverValidationOptions = {},
directiveResolvers = null,
}: IExecutableSchemaDefinition) {
const jsSchema = _generateSchema(
typeDefs,
resolvers,
logger,
allowUndefinedInResolve,
resolverValidationOptions,
directiveResolvers,
);
if (typeof resolvers['__schema'] === 'function') {
// TODO a bit of a hack now, better rewrite generateSchema to attach it there.
Expand Down Expand Up @@ -378,11 +388,12 @@ function addResolveFunctionsToSchema(
// is inside NPM
if (!(type as any).isValidValue(fieldName)) {
throw new SchemaError(
`${typeName}.${fieldName} was defined in resolvers, but enum is not in schema`,
`${typeName}.${fieldName} was defined in resolvers, but enum is not in schema`,
);
}

type.getValue(fieldName)['value'] = resolveFunctions[typeName][fieldName];
type.getValue(fieldName)['value'] =
resolveFunctions[typeName][fieldName];
return;
}

Expand Down Expand Up @@ -637,6 +648,62 @@ function runAtMostOncePerRequest(
};
}

function attachDirectiveResolvers(
schema: GraphQLSchema,
directiveResolvers: IDirectiveResolvers<any, any>,
) {
if (typeof directiveResolvers !== 'object') {
throw new Error(
`Expected directiveResolvers to be of type object, got ${typeof directiveResolvers}`,
);
}
if (Array.isArray(directiveResolvers)) {
throw new Error(
'Expected directiveResolvers to be of type object, got Array',
);
}
forEachField(schema, (field: GraphQLField<any, any>) => {
const directives = field.astNode.directives;
directives.forEach((directive: DirectiveNode) => {
const directiveName = directive.name.value;
const resolver = directiveResolvers[directiveName];

if (resolver) {
const originalResolver = field.resolve || defaultFieldResolver;
const Directive = schema.getDirective(directiveName);
if (typeof Directive === 'undefined') {
throw new Error(
`Directive @${directiveName} is undefined. ` +
'Please define in schema before using',
);
}
const directiveArgs = getArgumentValues(Directive, directive);

field.resolve = (...args: any[]) => {
const [source, , context, info] = args;
return resolver(
() => {
try {
const promise = originalResolver.call(field, ...args);
if (promise instanceof Promise) {
return promise;
}
return Promise.resolve(promise);
} catch (error) {
return Promise.reject(error);
}
},
source,
directiveArgs,
context,
info,
);
};
}
});
});
}

export {
makeExecutableSchema,
SchemaError,
Expand All @@ -650,4 +717,5 @@ export {
addSchemaLevelResolveFunction,
attachConnectorsToContext,
concatenateTypeDefs,
attachDirectiveResolvers,
};
2 changes: 1 addition & 1 deletion src/stitching/TypeRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default class TypeRegistry {
this.schemaByField = {
query: {},
mutation: {},
subscription: {}
subscription: {},
};
this.fragmentReplacements = {};
}
Expand Down
12 changes: 5 additions & 7 deletions src/stitching/linkToFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,11 @@ function execute(
link: ApolloLink,
operation: GraphQLRequest,
): Observable<FetchResult> {
return (
link.request(
createOperation(
operation.context,
transformOperation(validateOperation(operation)),
),
)
return link.request(
createOperation(
operation.context,
transformOperation(validateOperation(operation)),
),
);
}

Expand Down
Loading