From 76b2d15dc3b630a09dc8e5ce26b08fad39ffdebf Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 7 Nov 2018 03:30:18 -0500 Subject: [PATCH] Add schema directives support Signed-off-by: Arda TANRIKULU --- packages/core/src/graphql-module.ts | 75 +++++++++++++++++----- packages/core/tests/graphql-module.spec.ts | 64 +++++++++++++++++- 2 files changed, 121 insertions(+), 18 deletions(-) diff --git a/packages/core/src/graphql-module.ts b/packages/core/src/graphql-module.ts index d24d913029..7894ee9f31 100644 --- a/packages/core/src/graphql-module.ts +++ b/packages/core/src/graphql-module.ts @@ -1,4 +1,4 @@ -import { IResolvers, makeExecutableSchema } from 'graphql-tools'; +import { IResolvers, makeExecutableSchema, SchemaDirectiveVisitor } from 'graphql-tools'; import { mergeGraphQLSchemas, mergeResolvers } from '@graphql-modules/epoxy'; import { Provider, ModuleContext, Injector } from './di'; import { DocumentNode, print, GraphQLSchema } from 'graphql'; @@ -15,6 +15,12 @@ export type BuildContextFn = ( injector: Injector, ) => Promise | Context; +export interface ISchemaDirectives { + [name: string]: typeof SchemaDirectiveVisitor; +} + +export type ModulesMap = Map>; + /** * Defines the structure of a dependency as it declared in each module's `dependencies` field. */ @@ -62,6 +68,7 @@ export interface GraphQLModuleOptions { providers?: Provider[] | ((config: Config) => Provider[]); /** Object map between `Type.field` to a function(s) that will wrap the resolver of the field */ resolversComposition?: IResolversComposerMapping | ((config: Config) => IResolversComposerMapping); + schemaDirectives?: ISchemaDirectives | ((config: Config) => ISchemaDirectives); } /** @@ -80,8 +87,9 @@ export interface ModuleCache { schema: GraphQLSchema; typeDefs: string; resolvers: IResolvers; + schemaDirectives: ISchemaDirectives; contextBuilder: (req: Request) => Promise; - modulesMap: Map>; + modulesMap: ModulesMap; } /** @@ -98,6 +106,7 @@ export class GraphQLModule { schema: null, typeDefs: null, resolvers: null, + schemaDirectives: null, contextBuilder: null, modulesMap: null, }; @@ -165,14 +174,7 @@ export class GraphQLModule { return this._cache.typeDefs; } - get resolvers(): IResolvers { - if (!this._cache.resolvers) { - this.buildSchemaAndInjector(this.modulesMap); - } - return this._cache.resolvers; - } - - private buildTypeDefs(modulesMap: Map>) { + private buildTypeDefs(modulesMap: ModulesMap) { const typeDefsArr = []; const selfImports = this.selfImports; for (let module of selfImports) { @@ -191,6 +193,20 @@ export class GraphQLModule { this._cache.typeDefs = mergeGraphQLSchemas(typeDefsArr); } + get resolvers(): IResolvers { + if (!this._cache.resolvers) { + this.buildSchemaAndInjector(this.modulesMap); + } + return this._cache.resolvers; + } + + get schemaDirectives(): ISchemaDirectives { + if (!this._cache.schemaDirectives) { + this.buildSchemaAndInjector(this.modulesMap); + } + return this._cache.schemaDirectives; + } + /** * Returns the GraphQL type definitions of the module * @return a `string` with the merged type definitions @@ -284,12 +300,26 @@ export class GraphQLModule { return resolversComposition; } - private buildSchemaAndInjector(modulesMap: Map>) { + get selfSchemaDirectives(): ISchemaDirectives { + let schemaDirectives: ISchemaDirectives = {}; + const schemaDirectivesDefinitions = this._options.schemaDirectives; + if (schemaDirectivesDefinitions) { + if (typeof schemaDirectivesDefinitions === 'function') { + schemaDirectives = schemaDirectivesDefinitions(this._moduleConfig); + } else { + schemaDirectives = schemaDirectivesDefinitions; + } + } + return schemaDirectives; + } + + private buildSchemaAndInjector(modulesMap: ModulesMap) { const imports = this.selfImports; const importsTypeDefs = new Array(); const importsResolvers = new Array(); const importsInjectors = new Array(); const importsContextBuilders = new Array<(req: Request) => Promise>(); + let importsSchemaDirectives: ISchemaDirectives = {}; for (let module of imports) { const moduleName = typeof module === 'string' ? module : module.name; module = modulesMap.get(moduleName); @@ -304,6 +334,7 @@ export class GraphQLModule { const resolvers = module._cache.resolvers; const injector = module._cache.injector; const contextBuilder = module._cache.contextBuilder; + const schemaDirectives = module._cache.schemaDirectives; if (typeDefs && typeDefs.length) { importsTypeDefs.push(typeDefs); @@ -311,6 +342,7 @@ export class GraphQLModule { importsResolvers.push(resolvers); importsInjectors.push(injector); importsContextBuilders.push(contextBuilder); + importsSchemaDirectives = { ...importsSchemaDirectives, ...schemaDirectives }; } const injector = new Injector(); @@ -358,15 +390,25 @@ export class GraphQLModule { const allTypeDefs = [...importsTypeDefs]; const selfTypeDefs = this.selfTypeDefs; - if (selfTypeDefs && selfTypeDefs) { + if (selfTypeDefs) { allTypeDefs.push(selfTypeDefs); } + const mergedTypeDefs = mergeGraphQLSchemas( + allTypeDefs, + ); + + this._cache.typeDefs = mergedTypeDefs; + + const mergedSchemaDirectives = { + ...importsSchemaDirectives, + ...this.selfSchemaDirectives, + }; + + this._cache.schemaDirectives = mergedSchemaDirectives; + this._cache.schema = {} as GraphQLSchema; if (allTypeDefs.length) { - const mergedTypeDefs = mergeGraphQLSchemas( - allTypeDefs, - ); try { this._cache.schema = makeExecutableSchema({ typeDefs: mergedTypeDefs, @@ -378,6 +420,7 @@ export class GraphQLModule { requireResolversForResolveType: false, allowResolversNotInSchema: true, }, + schemaDirectives: mergedSchemaDirectives, }); } catch (e) { if (e.message !== 'Must provide typeDefs') { @@ -470,7 +513,7 @@ export class GraphQLModule { return modulesMap; } - private checkAndFixModulesMap(modulesMap: Map>): Map> { + private checkAndFixModulesMap(modulesMap: ModulesMap): Map> { const graph = new DepGraph>(); modulesMap.forEach(module => { diff --git a/packages/core/tests/graphql-module.spec.ts b/packages/core/tests/graphql-module.spec.ts index 5121b03530..0e9708b8b1 100644 --- a/packages/core/tests/graphql-module.spec.ts +++ b/packages/core/tests/graphql-module.spec.ts @@ -8,10 +8,11 @@ import { ModuleContext, OnRequest, } from '../src'; -import { execute, GraphQLSchema, printSchema } from 'graphql'; +import { execute, GraphQLSchema, printSchema, GraphQLField, GraphQLEnumValue, GraphQLString, defaultFieldResolver } from 'graphql'; import { stripWhitespaces } from './utils'; import gql from 'graphql-tag'; import { DependencyProviderNotFoundError, Injectable } from '../src'; +import { SchemaDirectiveVisitor } from 'graphql-tools'; describe('GraphQLModule', () => { // A @@ -522,7 +523,7 @@ describe('GraphQLModule', () => { interface MyBase { id: String } - + type MyType implements MyBase { id: String } @@ -556,4 +557,63 @@ describe('GraphQLModule', () => { expect(hasInjector).toBeTruthy(); }); }); + describe('Schema Directives', async () => { + it('should handle schema directives', async () => { + + const typeDefs = ` + directive @date on FIELD_DEFINITION + + scalar Date + + type Query { + today: Date @date + }`; + + class FormattableDateDirective extends SchemaDirectiveVisitor { + public visitFieldDefinition(field) { + const { resolve = defaultFieldResolver } = field; + + field.args.push({ + name: 'format', + type: GraphQLString, + }); + + field.resolve = async function( + source, + args, + context, + info, + ) { + const date = await resolve.call(this, source, args, context, info); + return date.toLocaleDateString(); + }; + + field.type = GraphQLString; + } + } + + const { schema, context } = new GraphQLModule({ + typeDefs, + resolvers: { + Query: { + today: () => new Date(), + }, + }, + schemaDirectives: { + date: FormattableDateDirective, + }, + }); + + const contextValue = await context({ req: {} }); + + const result = await execute({ + schema, + document: gql`query { today }`, + contextValue, + }); + + expect(result.data['today']).toEqual(new Date().toLocaleDateString()); + + }); + }); });