Skip to content

Commit

Permalink
Merge pull request #529 from apollographql/directives-staging
Browse files Browse the repository at this point in the history
Resolver directives continuation
  • Loading branch information
Sashko Stubailo authored Dec 12, 2017
2 parents f8ea3fd + 12e119d commit cc69bb1
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 23 deletions.
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

0 comments on commit cc69bb1

Please sign in to comment.