Skip to content

Commit

Permalink
add schema transforms
Browse files Browse the repository at this point in the history
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
  • Loading branch information
yaacovCR committed May 12, 2020
1 parent e4aa1e0 commit dd280b0
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 4 deletions.
5 changes: 5 additions & 0 deletions 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,6 +62,10 @@ 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) {
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
10 changes: 7 additions & 3 deletions packages/utils/src/Interfaces.ts
Expand Up @@ -119,10 +119,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
151 changes: 151 additions & 0 deletions packages/utils/tests/schemaTransforms.test.ts
@@ -0,0 +1,151 @@
import {
GraphQLObjectType,
GraphQLSchema,
printSchema,
} from 'graphql';

import { makeExecutableSchema } from '@graphql-tools/schema';
import {
SchemaTransform,
mapSchema,
MapperKind,
getDirectives,
} from '@graphql-tools/utils';

const typeDefs = `
directive @schemaDirective(role: String) on SCHEMA
directive @schemaExtensionDirective(role: String) on SCHEMA
directive @queryTypeDirective on OBJECT
directive @queryTypeExtensionDirective on OBJECT
directive @queryFieldDirective on FIELD_DEFINITION
directive @enumTypeDirective on ENUM
directive @enumTypeExtensionDirective on ENUM
directive @enumValueDirective on ENUM_VALUE
directive @dateDirective(tz: String) on SCALAR
directive @dateExtensionDirective(tz: String) on SCALAR
directive @interfaceDirective on INTERFACE
directive @interfaceExtensionDirective on INTERFACE
directive @interfaceFieldDirective on FIELD_DEFINITION
directive @inputTypeDirective on INPUT_OBJECT
directive @inputTypeExtensionDirective on INPUT_OBJECT
directive @inputFieldDirective on INPUT_FIELD_DEFINITION
directive @mutationTypeDirective on OBJECT
directive @mutationTypeExtensionDirective on OBJECT
directive @mutationArgumentDirective on ARGUMENT_DEFINITION
directive @mutationMethodDirective on FIELD_DEFINITION
directive @objectTypeDirective on OBJECT
directive @objectTypeExtensionDirective on OBJECT
directive @objectFieldDirective on FIELD_DEFINITION
directive @unionDirective on UNION
directive @unionExtensionDirective on UNION
schema @schemaDirective(role: "admin") {
query: Query
mutation: Mutation
}
extend schema @schemaExtensionDirective(role: "admin")
type Query @queryTypeDirective {
people: [Person] @queryFieldDirective
}
extend type Query @queryTypeExtensionDirective
enum Gender @enumTypeDirective {
NONBINARY @enumValueDirective
FEMALE
MALE
}
extend enum Gender @enumTypeExtensionDirective
scalar Date @dateDirective(tz: "utc")
extend scalar Date @dateExtensionDirective(tz: "utc")
interface Named @interfaceDirective {
name: String! @interfaceFieldDirective
}
extend interface Named @interfaceExtensionDirective
input PersonInput @inputTypeDirective {
name: String! @inputFieldDirective
gender: Gender
}
extend input PersonInput @inputTypeExtensionDirective
type Mutation @mutationTypeDirective {
addPerson(
input: PersonInput @mutationArgumentDirective
): Person @mutationMethodDirective
}
extend type Mutation @mutationTypeExtensionDirective
type Person implements Named @objectTypeDirective {
id: ID! @objectFieldDirective
name: String!
}
extend type Person @objectTypeExtensionDirective
union WhateverUnion @unionDirective = Person | Query | Mutation
extend union WhateverUnion @unionExtensionDirective`;

describe('@directives', () => {
test('can be iterated with mapSchema', () => {
const visited: Set<GraphQLObjectType> = new Set();

function addTypeToSetDirective(directiveNames: Array<string>): SchemaTransform {
return schema => mapSchema(schema, {
[MapperKind.OBJECT_TYPE]: type => {
const directives = getDirectives(schema, type);
Object.keys(directives).forEach(directiveName => {
if (directiveNames.includes(directiveName)) {
expect(type.name).toBe(schema.getQueryType().name);
visited.add(type);
}
});
return undefined;
}
})
}

makeExecutableSchema({
typeDefs,
schemaTransforms: [
addTypeToSetDirective(['queryTypeDirective', 'queryTypeExtensionDirective'])
]
});

expect(visited.size).toBe(1);
});

test('can visit the schema directly', () => {
const visited: Array<GraphQLSchema> = [];

function recordDirectiveUses(directiveNames: Array<string>): SchemaTransform {
return schema => {
const directives = getDirectives(schema, schema);
Object.keys(directives).forEach(directiveName => {
if (directiveNames.includes(directiveName)) {
visited.push(schema);
}
});
return schema;
}
}

const schema = makeExecutableSchema({
typeDefs,
schemaTransforms: [
recordDirectiveUses(['schemaDirective', 'schemaExtensionDirective'])
]
});

const printedSchema = printSchema(makeExecutableSchema({ typeDefs }));
expect(printSchema(schema)).toEqual(printedSchema);

expect(visited.length).toBe(2);
expect(printSchema(visited[0])).toEqual(printedSchema);
expect(printSchema(visited[1])).toEqual(printedSchema);
});
});

0 comments on commit dd280b0

Please sign in to comment.