diff --git a/docs/guides/data-loader.md b/docs/guides/data-loader.md index e7437769a3..73feef754b 100644 --- a/docs/guides/data-loader.md +++ b/docs/guides/data-loader.md @@ -39,7 +39,7 @@ You can see how to generate DataLoader in GraphQLModules using factory functions scope: ProviderScope.Session provide: MY_DATA_LOADER, useFactory: - Inject(ModuleSessionInfo)( + InjectFunction(ModuleSessionInfo)( // Use Dependency Injection to get ModuleSessionInfo to access network session ({ session }) => new DataLoader( ids => genUsers(session.req.authToken, ids) diff --git a/docs/introduction/dependency-injection.md b/docs/introduction/dependency-injection.md index 574e42d170..577ba2c1ee 100644 --- a/docs/introduction/dependency-injection.md +++ b/docs/introduction/dependency-injection.md @@ -228,7 +228,7 @@ Example; ```typescript import { Injectable } from '@graphql-modules/di'; -import { OnRequest } from '@graphql-modules/core'; +import { OnInit } from '@graphql-modules/core'; @Injectable() export class DatabaseProvider implements OnInit { constructor(private dbClient: DbClient) {} @@ -244,7 +244,8 @@ export class DatabaseProvider implements OnInit { You can get access to useful information: the top `GraphQLModule` instance, GraphQL Context, and the network session by defining this hook as a method in your class provider. ```typescript -import { Injectable, OnRequest } from '@graphql-modules/core'; +import { Injectable } from '@graphql-modules/di'; +import { OnRequest } from '@graphql-modules/core'; Example; @@ -273,7 +274,8 @@ It takes same parameter like `OnRequest` hook but it gets called even before the Example; ```typescript -import { Injectable, OnResponse } from '@graphql-modules/core'; +import { Injectable } from '@graphql-modules/di'; +import { OnResponse } from '@graphql-modules/core'; @Injectable() export class MyProvider implements OnResponse { @@ -314,7 +316,8 @@ This hook is similar to `OnRequest` hook, but this is called on the initializati Example; ```typescript -import { Injectable, OnConnect } from '@graphql-modules/core'; +import { Injectable } from '@graphql-modules/di'; +import { OnConnect } from '@graphql-modules/core'; @Injectable({ scope: ProviderScope.Session @@ -339,7 +342,8 @@ This hook is similar to `OnResponse` hook, but this is called on the termination [You can learn more from Apollo docs.](https://www.apollographql.com/docs/graphql-subscriptions/authentication.html) ```typescript -import { Injectable, OnDisconnect } from '@graphql-modules/core'; +import { Injectable } from '@graphql-modules/di'; +import { OnDisconnect } from '@graphql-modules/core'; @Injectable() export class MyProvider implements OnDisconnect { @@ -396,4 +400,4 @@ You can see more about scoped providers; ## Built-in `ModuleSessionInfo` Provider -Every GraphQL-Module creates a `ModuleSessionInfo` instance in each network request that contains raw Request from the GraphQL Server, `SessionInjector` that contains Session-scoped instances together with Application-scoped ones and `Context` object which is constructed with `contextBuilder` of the module. But, notice that you cannot use this built-in provider. +Every GraphQL-Module creates a `ModuleSessionInfo` instance in each network request that contains raw Request from the GraphQL Server, `SessionInjector` that contains Session-scoped instances together with Application-scoped ones and `Context` object which is constructed with `contextBuilder` of the module. But, notice that you cannot use this built-in provider in Application Scope. diff --git a/packages/core/package.json b/packages/core/package.json index 9ac1b058f0..81c22dd298 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -32,10 +32,13 @@ }, "dependencies": { "@graphql-modules/di": "0.6.5", - "graphql-toolkit": "0.2.6", + "graphql-toolkit": "0.2.7", "graphql-tools": "4.0.4", "tslib": "1.9.3" }, + "peerDependencies": { + "graphql": "^14.1.1" + }, "devDependencies": { "apollo-cache-inmemory": "1.4.2", "apollo-client": "2.4.12", diff --git a/packages/core/src/graphql-module.ts b/packages/core/src/graphql-module.ts index e844acc1fe..9f5fa939d3 100644 --- a/packages/core/src/graphql-module.ts +++ b/packages/core/src/graphql-module.ts @@ -1,17 +1,17 @@ import { IResolvers, SchemaDirectiveVisitor, - mergeSchemas, IDirectiveResolvers, - makeExecutableSchema, IResolverValidationOptions, } from 'graphql-tools'; import { mergeResolvers, IResolversComposerMapping, composeResolvers, + mergeSchemas, getSchemaDirectiveFromDirectiveResolver, mergeTypeDefs, + ResolversCompositionFn, } from 'graphql-toolkit'; import { Provider, Injector, ProviderScope } from '@graphql-modules/di'; import { DocumentNode, GraphQLSchema, parse, GraphQLScalarType } from 'graphql'; @@ -73,7 +73,7 @@ export interface GraphQLModuleOptions { * You can also pass a function that will get the module's config as argument, and should return * the type definitions. */ - typeDefs?: GraphQLModuleOption; + typeDefs?: GraphQLModuleOption, Config, Session, Context>; /** * Resolvers object, or a function will get the module's config as argument, and should * return the resolvers object. @@ -246,124 +246,61 @@ export class GraphQLModule(); + /** + * Gets the application `GraphQLSchema` object. + * If the schema object is not built yet, it compiles + * the `typeDefs` and `resolvers` into `GraphQLSchema` + */ + get schema() { + if (typeof this._cache.schema === 'undefined') { + this.checkConfiguration(); + const importsSchemas = new Array(); const selfImports = this.selfImports; for (const module of selfImports) { - if (typeof module === 'undefined') { - throw new DependencyModuleUndefinedError(this.name); - } const moduleSchema = module.schema; if (moduleSchema) { - schemaSet.add(moduleSchema); + importsSchemas.push(moduleSchema); } } - const selfTypeDefs = this.selfTypeDefs; - let resolvers = {}; - const schemaDirectives = this.schemaDirectives; try { - if (selfTypeDefs) { - const localSchema = buildASTSchema(selfTypeDefs, { - assumeValid: true, - assumeValidSDL: true, - }); - schemaSet.add(localSchema); - } - const selfResolversComposition = this.selfResolversComposition; - if (Object.keys(selfResolversComposition).length) { - resolvers = this.resolvers; - } else { - resolvers = this.addSessionInjectorToSelfResolversContext(); - } + const selfTypeDefs = this.selfTypeDefs; + const selfEncapsulatedResolvers = this.addSessionInjectorToSelfResolversContext(); + const selfEncapsulatedResolversComposition = this.addSessionInjectorToSelfResolversCompositionContext(); + const selfLogger = this.selfLogger; + const selfResolverValidationOptions = this.selfResolverValidationOptions; const selfExtraSchemas = this.selfExtraSchemas; - const schemas = [...selfExtraSchemas, ...schemaSet]; - if (schemas.length) { + if (importsSchemas.length || selfTypeDefs || selfExtraSchemas.length) { this._cache.schema = mergeSchemas({ - schemas, - resolvers, - schemaDirectives, + schemas: [ + ...importsSchemas, + ...selfExtraSchemas, + ], + typeDefs: selfTypeDefs || undefined, + resolvers: selfEncapsulatedResolvers, + resolversComposition: selfEncapsulatedResolversComposition, + resolverValidationOptions: selfResolverValidationOptions, + logger: 'clientError' in selfLogger ? { + log: message => selfLogger.clientError(message), + } : undefined, }); } else { this._cache.schema = null; } } catch (e) { - if (e.message.includes(`Type "`) && e.message.includes(`" not found in document.`)) { + if (e.message === 'Must provide typeDefs') { + this._cache.schema = null; + } else if (e.message.includes(`Type "`) && e.message.includes(`" not found in document.`)) { const typeDef = e.message.replace('Type "', '').replace('" not found in document.', ''); throw new TypeDefNotFoundError(typeDef, this.name); } else { throw new SchemaNotValidError(this.name, e.message); } } - } - */ - buildSchemaWithMakeExecutableSchema() { - this.checkConfiguration(); - const selfImports = this.selfImports; - // Do iterations once - for (const module of selfImports) { - if (typeof module._cache.schema === 'undefined') { - module.buildSchemaWithMakeExecutableSchema(); - } - } - try { - const typeDefs = this.typeDefs; - const resolvers = this.resolvers; - const schemaDirectives = this.schemaDirectives; - const logger = this.selfLogger; - const resolverValidationOptions = this.selfResolverValidationOptions; - const extraSchemas = this.extraSchemas; - if (typeDefs) { - const localSchema = makeExecutableSchema>({ - typeDefs, - resolvers, - schemaDirectives, - logger: - 'clientError' in logger - ? { - log: message => logger.clientError(message), - } - : undefined, - resolverValidationOptions, - }); - if (extraSchemas.length) { - this._cache.schema = mergeSchemas({ - schemas: [localSchema, ...extraSchemas], - }); - } else { - this._cache.schema = localSchema; - } - } else { - this._cache.schema = null; - } - } catch (e) { - if (e.message === 'Must provide typeDefs') { - this._cache.schema = null; - } else if (e.message.includes(`Type "`) && e.message.includes(`" not found in document.`)) { - const typeDef = e.message.replace('Type "', '').replace('" not found in document.', ''); - throw new TypeDefNotFoundError(typeDef, this.name); - } else { - throw new SchemaNotValidError(this.name, e.message); - } - } - if ('middleware' in this._options) { - const middlewareResult = this.injector.call(this._options.middleware, this); - if (middlewareResult) { + if ('middleware' in this._options) { + const middlewareResult = this.injector.call(this._options.middleware, this); Object.assign(this._cache, middlewareResult); } } - } - - /** - * Gets the application `GraphQLSchema` object. - * If the schema object is not built yet, it compiles - * the `typeDefs` and `resolvers` into `GraphQLSchema` - */ - get schema() { - if (typeof this._cache.schema === 'undefined') { - this.buildSchemaWithMakeExecutableSchema(); - // this.buildSchemaWithMergeSchemas(); - } return this._cache.schema; } @@ -572,9 +509,12 @@ export class GraphQLModule typeDefsDefinition); + if (typeDefsDefinitions.length) { + typeDefs = mergeTypeDefs(typeDefsDefinitions, { + useSchemaDefinition: false, + }); + } } else if (typeDefsDefinitions) { typeDefs = typeDefsDefinitions; } @@ -760,10 +700,8 @@ export class GraphQLModule>) => { + return [ (next: any) => async (root: any, args: any, appContext: any, info: any) => { if (appContext instanceof Promise) { appContext = await appContext; @@ -786,6 +724,19 @@ export class GraphQLModule { } } - const { schema } = new GraphQLModule({ + const { schema, schemaDirectives } = new GraphQLModule({ typeDefs, resolvers: { Query: { @@ -827,6 +827,8 @@ describe('GraphQLModule', () => { }, }); + SchemaDirectiveVisitor.visitSchemaDirectives(schema, schemaDirectives); + const result = await execute({ schema, @@ -870,7 +872,7 @@ describe('GraphQLModule', () => { }, }); - const { schema } = new GraphQLModule({ + const VisitedDateModule = new GraphQLModule({ typeDefs: gql` scalar Date @@ -888,9 +890,17 @@ describe('GraphQLModule', () => { ], }); + const { schema, schemaDirectives } = new GraphQLModule({ + imports: [ + DateDirectiveModule, + VisitedDateModule, + ], + }); + + SchemaDirectiveVisitor.visitSchemaDirectives(schema, schemaDirectives); + const result = await execute({ schema, - document: gql`query { today }`, }); @@ -1088,23 +1098,50 @@ describe('GraphQLModule', () => { const { schema, context } = new GraphQLModule({ typeDefs: gql` type Query { - foo: Boolean + isDirty: Boolean } `, resolvers: { Query: { - foo: (root, args, context, info) => !!info.schema['__DIRTY__'], + isDirty: (root, args, context, info) => !!info.schema['__DIRTY__'], }, }, middleware: ({ schema }) => { schema['__DIRTY__'] = true; return { schema }; }, }); const result = await execute({ schema, - document: gql`query { foo }`, + document: gql`query { isDirty }`, + contextValue: await context({ req: {} }), + }); + expect(result.errors).toBeFalsy(); + expect(result.data['isDirty']).toBeTruthy(); + }); + it('should encapsulate the schema mutations using middleware', async () => { + const FooModule = new GraphQLModule({ + typeDefs: gql` + type Query { + isDirty: Boolean + } + `, + resolvers: { + Query: { + isDirty: (root, args, context, info) => !!info.schema['__DIRTY__'], + }, + }, + middleware: ({schema}) => { schema['__DIRTY__'] = true; return { schema }; }, + }); + const { schema, context } = new GraphQLModule({ + imports: [ + FooModule, + ], + }); + const result = await execute({ + schema, + document: gql`query { isDirty }`, contextValue: await context({ req: {} }), }); expect(result.errors).toBeFalsy(); - expect(result.data['foo']).toBeTruthy(); + expect(result.data['isDirty']).toBeTruthy(); }); it('should avoid getting non-configured module', async () => { const FOO = Symbol('FOO'); @@ -1356,4 +1393,25 @@ describe('GraphQLModule', () => { }); expect(data.foo).toBe('FOO'); }); + it('should generate schemaless module if an empty array typeDefs specified', async () => { + const { schema } = new GraphQLModule({ + typeDefs: [], + resolvers: {}, + }); + expect(schema).toBeNull(); + }); + it('should generate schemaless module if empty string typeDefs specified', async () => { + const { schema } = new GraphQLModule({ + typeDefs: '', + resolvers: {}, + }); + expect(schema).toBeNull(); + }); + it('should generate schemaless module if an array with an empty string typeDefs specified', async () => { + const { schema } = new GraphQLModule({ + typeDefs: [''], + resolvers: {}, + }); + expect(schema).toBeNull(); + }); }); diff --git a/packages/graphql-modules/package.json b/packages/graphql-modules/package.json index 3bcc849754..1bfd7338d8 100644 --- a/packages/graphql-modules/package.json +++ b/packages/graphql-modules/package.json @@ -43,6 +43,7 @@ "@graphql-modules/core": "0.6.5", "tslib": "1.9.3" }, + "sideEffects": false, "main": "dist/commonjs/index.js", "module": "dist/esnext/index.js", "typings": "dist/esnext/index.d.ts", diff --git a/yarn.lock b/yarn.lock index 7abba267a3..fedd75d78b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2230,16 +2230,17 @@ graphql-tag@2.10.1: resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02" integrity sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg== -graphql-toolkit@0.2.6: - version "0.2.6" - resolved "https://registry.yarnpkg.com/graphql-toolkit/-/graphql-toolkit-0.2.6.tgz#539f82450c85d81ecb81b5c825d0c7271b458ece" - integrity sha512-VqvsKHYxKW3/qoJIaO17JH6AfClHcPvZRMN2rG/y6fsTKCyCCS7P1+7aKu6RPUwFkW+reqxxrXjEGOG2bkCcMQ== +graphql-toolkit@0.2.7-8431ebe.0: + version "0.2.7-8431ebe.0" + resolved "https://registry.yarnpkg.com/graphql-toolkit/-/graphql-toolkit-0.2.7-8431ebe.0.tgz#fc769f66398439236991957f435e0d9c4d82e06f" + integrity sha512-ALGvI6j7/pcoRrHmgKCrRX9jb1xR3O2AAognRWcU14HuMll9IQwnytl3Qnlx+P/xpV4ZJsJMdTTvUkICVxUclA== dependencies: aggregate-error "2.2.0" deepmerge "3.2.0" glob "7.1.3" graphql-import "0.7.1" graphql-tag-pluck "0.7.0" + graphql-tools "4.0.4" is-glob "4.0.0" is-valid-path "0.1.1" lodash "4.17.11"