Skip to content

Commit

Permalink
Schema transforms (#1463)
Browse files Browse the repository at this point in the history
* rewrite attachDirectiveResolvers to be immutable

* add mapping by arguments

* add schema transforms

schema transform functions take a schema as an argument and return a schema, possibly modifying it

a schema transform function can be templated, i.e. one can create a function that takes certain arguments, such as the names of directives that annotate fields of interest, and return a schema transform function that modifies the schema based on the specified annotating directives

this allows one to reuse a schema transform function across projects, allowing for the customization of the schema transform function on a per use basis

schema transform functionc can be called directly on a schema to modify it before use -- makeExecutableSchema can now also be passed an array of schema transform functions, with makeExecutableSchema responsible for performing all schema transformations prior to returning.

Eventually, all schema visitor examples can be rewritten using the schema transform functional API

addresses #1234

* add generic field mapper to SchemaMapper to allow indiscriminate mapping of all fields and input fields

* add enum directive extraction and value mapping

* add tests demonstrating templated schema transforms

* update docs for schemaTransforms

include deprecated example in tests

reorder some types
  • Loading branch information
yaacovCR committed May 13, 2020
1 parent e0e3162 commit 0b7ac8a
Show file tree
Hide file tree
Showing 15 changed files with 2,727 additions and 553 deletions.
67 changes: 36 additions & 31 deletions packages/schema/src/attachDirectiveResolvers.ts
@@ -1,8 +1,11 @@
import { GraphQLSchema, GraphQLField, defaultFieldResolver } from 'graphql';
import { GraphQLSchema, defaultFieldResolver } from 'graphql';

import { IDirectiveResolvers, SchemaDirectiveVisitor } from '@graphql-tools/utils';
import { IDirectiveResolvers, mapSchema, MapperKind, getDirectives } from '@graphql-tools/utils';

export function attachDirectiveResolvers(schema: GraphQLSchema, directiveResolvers: IDirectiveResolvers) {
export function attachDirectiveResolvers(
schema: GraphQLSchema,
directiveResolvers: IDirectiveResolvers
): GraphQLSchema {
if (typeof directiveResolvers !== 'object') {
throw new Error(`Expected directiveResolvers to be of type object, got ${typeof directiveResolvers}`);
}
Expand All @@ -11,34 +14,36 @@ export function attachDirectiveResolvers(schema: GraphQLSchema, directiveResolve
throw new Error('Expected directiveResolvers to be of type object, got Array');
}

const schemaDirectives = Object.create(null);
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: fieldConfig => {
const newFieldConfig = { ...fieldConfig };

Object.keys(directiveResolvers).forEach(directiveName => {
schemaDirectives[directiveName] = class extends SchemaDirectiveVisitor {
public visitFieldDefinition(field: GraphQLField<any, any>) {
const resolver = directiveResolvers[directiveName];
const originalResolver = field.resolve != null ? field.resolve : defaultFieldResolver;
const directiveArgs = this.args;
field.resolve = (...args) => {
const [source /* original args */, , context, info] = args;
return resolver(
() =>
new Promise((resolve, reject) => {
const result = originalResolver.apply(field, args);
if (result instanceof Error) {
reject(result);
}
resolve(result);
}),
source,
directiveArgs,
context,
info
);
};
}
};
});
const directives = getDirectives(schema, fieldConfig);
Object.keys(directives).forEach(directiveName => {
if (directiveResolvers[directiveName]) {
const resolver = directiveResolvers[directiveName];
const originalResolver = newFieldConfig.resolve != null ? newFieldConfig.resolve : defaultFieldResolver;
const directiveArgs = directives[directiveName];
newFieldConfig.resolve = (source, originalArgs, context, info) => {
return resolver(
() =>
new Promise((resolve, reject) => {
const result = originalResolver(source, originalArgs, context, info);
if (result instanceof Error) {
reject(result);
}
resolve(result);
}),
source,
directiveArgs,
context,
info
);
};
}
});

SchemaDirectiveVisitor.visitSchemaDirectives(schema, schemaDirectives);
return newFieldConfig;
},
});
}
7 changes: 6 additions & 1 deletion packages/schema/src/makeExecutableSchema.ts
Expand Up @@ -19,6 +19,7 @@ export function makeExecutableSchema<TContext = any>({
resolverValidationOptions = {},
directiveResolvers,
schemaDirectives,
schemaTransforms = [],
parseOptions = {},
inheritResolversFromInterfaces = false,
}: IExecutableSchemaDefinition<TContext>) {
Expand Down Expand Up @@ -61,10 +62,14 @@ export function makeExecutableSchema<TContext = any>({
schema = addSchemaLevelResolver(schema, resolvers['__schema'] as GraphQLFieldResolver<any, any>);
}

schemaTransforms.forEach(schemaTransform => {
schema = schemaTransform(schema);
});

// directive resolvers are implemented using SchemaDirectiveVisitor.visitSchemaDirectives
// schema visiting modifies the schema in place
if (directiveResolvers != null) {
attachDirectiveResolvers(schema, directiveResolvers);
schema = attachDirectiveResolvers(schema, directiveResolvers);
}

if (schemaDirectives != null) {
Expand Down
2 changes: 2 additions & 0 deletions packages/schema/src/types.ts
Expand Up @@ -5,6 +5,7 @@ import {
IDirectiveResolvers,
SchemaDirectiveVisitorClass,
GraphQLParseOptions,
SchemaTransform,
} from 'packages/utils/src';

export interface ILogger {
Expand All @@ -19,6 +20,7 @@ export interface IExecutableSchemaDefinition<TContext = any> {
resolverValidationOptions?: IResolverValidationOptions;
directiveResolvers?: IDirectiveResolvers<any, TContext>;
schemaDirectives?: Record<string, SchemaDirectiveVisitorClass>;
schemaTransforms?: Array<SchemaTransform>;
parseOptions?: GraphQLParseOptions;
inheritResolversFromInterfaces?: boolean;
}
7 changes: 6 additions & 1 deletion packages/stitch/src/stitchSchemas.ts
Expand Up @@ -43,6 +43,7 @@ export function stitchSchemas({
allowUndefinedInResolve = true,
resolverValidationOptions = {},
directiveResolvers,
schemaTransforms = [],
parseOptions = {},
}: IStitchSchemasOptions): GraphQLSchema {
if (typeof resolverValidationOptions !== 'object') {
Expand Down Expand Up @@ -157,8 +158,12 @@ export function stitchSchemas({
schema = addSchemaLevelResolver(schema, finalResolvers['__schema']);
}

schemaTransforms.forEach(schemaTransform => {
schema = schemaTransform(schema);
});

if (directiveResolvers != null) {
attachDirectiveResolvers(schema, directiveResolvers);
schema = attachDirectiveResolvers(schema, directiveResolvers);
}

if (schemaDirectives != null) {
Expand Down
32 changes: 28 additions & 4 deletions packages/utils/src/Interfaces.ts
Expand Up @@ -26,6 +26,8 @@ import {
FieldDefinitionNode,
GraphQLFieldConfig,
GraphQLInputFieldConfig,
GraphQLArgumentConfig,
GraphQLEnumValueConfig,
} from 'graphql';

import { SchemaVisitor } from './SchemaVisitor';
Expand Down Expand Up @@ -118,10 +120,14 @@ export interface IFieldResolverOptions<TSource = any, TContext = any, TArgs = an
astNode?: FieldDefinitionNode;
}

export type SchemaTransform = (originalSchema: GraphQLSchema) => GraphQLSchema;
export type RequestTransform = (originalRequest: Request) => Request;
export type ResultTransform = (originalResult: ExecutionResult) => ExecutionResult;

export interface Transform {
transformSchema?: (originalSchema: GraphQLSchema) => GraphQLSchema;
transformRequest?: (originalRequest: Request) => Request;
transformResult?: (originalResult: ExecutionResult) => ExecutionResult;
transformSchema?: SchemaTransform;
transformRequest?: RequestTransform;
transformResult?: ResultTransform;
}

export type FieldNodeMapper = (
Expand Down Expand Up @@ -367,6 +373,8 @@ export enum MapperKind {
SUBSCRIPTION_ROOT_FIELD = 'MapperKind.SUBSCRIPTION_ROOT_FIELD',
INTERFACE_FIELD = 'MapperKind.INTERFACE_FIELD',
INPUT_OBJECT_FIELD = 'MapperKind.INPUT_OBJECT_FIELD',
ARGUMENT = 'MapperKind.ARGUMENT',
ENUM_VALUE = 'MapperKind.ENUM_VALUE',
}

export interface SchemaMapper {
Expand All @@ -383,15 +391,18 @@ export interface SchemaMapper {
[MapperKind.QUERY]?: ObjectTypeMapper;
[MapperKind.MUTATION]?: ObjectTypeMapper;
[MapperKind.SUBSCRIPTION]?: ObjectTypeMapper;
[MapperKind.DIRECTIVE]?: DirectiveMapper;
[MapperKind.ENUM_VALUE]?: EnumValueMapper;
[MapperKind.FIELD]?: GenericFieldMapper<GraphQLFieldConfig<any, any> | GraphQLInputFieldConfig>;
[MapperKind.OBJECT_FIELD]?: FieldMapper;
[MapperKind.ROOT_FIELD]?: FieldMapper;
[MapperKind.QUERY_ROOT_FIELD]?: FieldMapper;
[MapperKind.MUTATION_ROOT_FIELD]?: FieldMapper;
[MapperKind.SUBSCRIPTION_ROOT_FIELD]?: FieldMapper;
[MapperKind.INTERFACE_FIELD]?: FieldMapper;
[MapperKind.COMPOSITE_FIELD]?: FieldMapper;
[MapperKind.ARGUMENT]?: ArgumentMapper;
[MapperKind.INPUT_OBJECT_FIELD]?: InputFieldMapper;
[MapperKind.DIRECTIVE]?: DirectiveMapper;
}

export type NamedTypeMapper = (type: GraphQLNamedType, schema: GraphQLSchema) => GraphQLNamedType | null | undefined;
Expand All @@ -400,6 +411,12 @@ export type ScalarTypeMapper = (type: GraphQLScalarType, schema: GraphQLSchema)

export type EnumTypeMapper = (type: GraphQLEnumType, schema: GraphQLSchema) => GraphQLEnumType | null | undefined;

export type EnumValueMapper = (
value: GraphQLEnumValueConfig,
typeName: string,
schema: GraphQLSchema
) => GraphQLEnumValueConfig | [string, GraphQLEnumValueConfig] | null | undefined;

export type CompositeTypeMapper = (
type: GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType,
schema: GraphQLSchema
Expand Down Expand Up @@ -438,4 +455,11 @@ export type GenericFieldMapper<F extends GraphQLFieldConfig<any, any> | GraphQLI

export type FieldMapper = GenericFieldMapper<GraphQLFieldConfig<any, any>>;

export type ArgumentMapper = (
argumentConfig: GraphQLArgumentConfig,
fieldName: string,
typeName: string,
schema: GraphQLSchema
) => GraphQLArgumentConfig | [string, GraphQLArgumentConfig] | null | undefined;

export type InputFieldMapper = GenericFieldMapper<GraphQLInputFieldConfig>;
107 changes: 98 additions & 9 deletions packages/utils/src/get-directives.ts
@@ -1,22 +1,111 @@
import { getDirectiveValues, GraphQLDirective, GraphQLSchema } from 'graphql';
import {
GraphQLDirective,
GraphQLSchema,
SchemaDefinitionNode,
TypeDefinitionNode,
SchemaExtensionNode,
TypeExtensionNode,
GraphQLNamedType,
GraphQLField,
GraphQLInputField,
FieldDefinitionNode,
InputValueDefinitionNode,
GraphQLFieldConfig,
GraphQLInputFieldConfig,
GraphQLSchemaConfig,
GraphQLObjectTypeConfig,
GraphQLInterfaceTypeConfig,
GraphQLUnionTypeConfig,
GraphQLScalarTypeConfig,
GraphQLEnumTypeConfig,
GraphQLInputObjectTypeConfig,
GraphQLEnumValue,
GraphQLEnumValueConfig,
EnumValueDefinitionNode,
} from 'graphql';

import { getArgumentValues } from './getArgumentValues';

export type DirectiveUseMap = { [key: string]: any };

export function getDirectives(schema: GraphQLSchema, node: any): DirectiveUseMap {
type SchemaOrTypeNode =
| SchemaDefinitionNode
| SchemaExtensionNode
| TypeDefinitionNode
| TypeExtensionNode
| EnumValueDefinitionNode
| FieldDefinitionNode
| InputValueDefinitionNode;

type DirectableGraphQLObject =
| GraphQLSchema
| GraphQLSchemaConfig
| GraphQLNamedType
| GraphQLObjectTypeConfig<any, any>
| GraphQLInterfaceTypeConfig<any, any>
| GraphQLUnionTypeConfig<any, any>
| GraphQLScalarTypeConfig<any, any>
| GraphQLEnumTypeConfig
| GraphQLEnumValue
| GraphQLEnumValueConfig
| GraphQLInputObjectTypeConfig
| GraphQLField<any, any>
| GraphQLInputField
| GraphQLFieldConfig<any, any>
| GraphQLInputFieldConfig;

export function getDirectives(schema: GraphQLSchema, node: DirectableGraphQLObject): DirectiveUseMap {
const schemaDirectives: ReadonlyArray<GraphQLDirective> =
schema && schema.getDirectives ? schema.getDirectives() : [];
const astNode = node && node.astNode;

const schemaDirectiveMap = schemaDirectives.reduce((schemaDirectiveMap, schemaDirective) => {
schemaDirectiveMap[schemaDirective.name] = schemaDirective;
return schemaDirectiveMap;
}, {});

let astNodes: Array<SchemaOrTypeNode> = [];
if (node.astNode) {
astNodes.push(node.astNode);
}
if ('extensionASTNodes' in node && node.extensionASTNodes) {
astNodes = [...astNodes, ...node.extensionASTNodes];
}

const result: DirectiveUseMap = {};

if (astNode) {
schemaDirectives.forEach((directive: GraphQLDirective) => {
const directiveValue = getDirectiveValues(directive, astNode);
astNodes.forEach(astNode => {
astNode.directives.forEach(directive => {
const schemaDirective = schemaDirectiveMap[directive.name.value];
if (schemaDirective) {
const directiveValue = getDirectiveValues(schemaDirective, astNode);

if (directiveValue !== undefined) {
result[directive.name] = directiveValue || {};
if (schemaDirective.isRepeatable) {
if (result[schemaDirective.name]) {
result[schemaDirective.name] = result[schemaDirective.name].concat([directiveValue]);
} else {
result[schemaDirective.name] = [directiveValue];
}
} else {
result[schemaDirective.name] = directiveValue;
}
}
});
}
});

return result;
}

// graphql-js getDirectiveValues does not handle repeatable directives
function getDirectiveValues(directiveDef: GraphQLDirective, node: SchemaOrTypeNode): any {
if (node.directives) {
if (directiveDef.isRepeatable) {
const directiveNodes = node.directives.filter(directive => directive.name.value === directiveDef.name);

return directiveNodes.map(directiveNode => getArgumentValues(directiveDef, directiveNode));
}

const directiveNode = node.directives.find(directive => directive.name.value === directiveDef.name);

return getArgumentValues(directiveDef, directiveNode);
}
}

0 comments on commit 0b7ac8a

Please sign in to comment.