diff --git a/packages/core/src/di/index.ts b/packages/core/src/di/index.ts index 26a398ab0e..e83a4a9116 100644 --- a/packages/core/src/di/index.ts +++ b/packages/core/src/di/index.ts @@ -3,3 +3,4 @@ export { Inject } from './inject'; export { Injectable } from './injectable'; export { Injector } from './injector'; export * from './types'; +export * from './utils'; diff --git a/packages/core/src/di/inject-many.ts b/packages/core/src/di/inject-many.ts new file mode 100644 index 0000000000..5766608040 --- /dev/null +++ b/packages/core/src/di/inject-many.ts @@ -0,0 +1,21 @@ +import { ServiceIdentifier } from './types'; + +import { DESIGN_PARAM_TYPES } from './utils'; + +declare var Reflect: any; + +export function InjectMany(serviceIdentifiers: Array>) { + return (target: any, _targetKey: any, index: number) => { + let dependencies = Reflect.getMetadata(DESIGN_PARAM_TYPES, target) || []; + if (!dependencies) { + throw new Error('You must decorate the provider class with @Injectable()'); + } + if (typeof index === 'number') { + dependencies[index] = serviceIdentifiers; + } else { + dependencies = serviceIdentifiers; + } + Reflect.defineMetadata(DESIGN_PARAM_TYPES, dependencies, target); + return target; + }; +} diff --git a/packages/core/src/di/inject.ts b/packages/core/src/di/inject.ts index 59be18ab7b..1a4e9439d1 100644 --- a/packages/core/src/di/inject.ts +++ b/packages/core/src/di/inject.ts @@ -1,6 +1,6 @@ import { ServiceIdentifier } from './types'; -import { DESIGN_PARAM_TYPES } from '../utils'; +import { DESIGN_PARAM_TYPES } from './utils'; declare var Reflect: any; diff --git a/packages/core/src/di/injectable.ts b/packages/core/src/di/injectable.ts index 8fdca4ab7d..4dcca1c8a1 100644 --- a/packages/core/src/di/injectable.ts +++ b/packages/core/src/di/injectable.ts @@ -1,4 +1,4 @@ -import { DESIGN_PARAM_TYPES } from '../utils'; +import { DESIGN_PARAM_TYPES } from './utils'; declare var Reflect: any; diff --git a/packages/core/src/di/injector.ts b/packages/core/src/di/injector.ts index 8f4c6a80e8..f91bd8dcb4 100644 --- a/packages/core/src/di/injector.ts +++ b/packages/core/src/di/injector.ts @@ -1,5 +1,5 @@ import { Provider, ServiceIdentifier, Factory, OnRequest } from './types'; -import { isType, DESIGN_PARAM_TYPES, isValueProvider, isClassProvider, isFactoryProvider, isTypeProvider } from '../utils'; +import { isType, DESIGN_PARAM_TYPES, isValueProvider, isClassProvider, isFactoryProvider, isTypeProvider } from './utils'; import { GraphQLModule } from '../graphql-module'; import { ServiceIdentifierNotFoundError, DependencyProviderNotFoundError, ProviderNotValidError } from '../errors'; diff --git a/packages/core/src/di/utils.ts b/packages/core/src/di/utils.ts new file mode 100644 index 0000000000..f3b5bc9cc9 --- /dev/null +++ b/packages/core/src/di/utils.ts @@ -0,0 +1,31 @@ +import { ServiceIdentifier, Provider, Type, ValueProvider, ClassProvider, FactoryProvider, TypeProvider } from './types'; + +export const DESIGN_PARAM_TYPES = 'design:paramtypes'; + +export function getServiceIdentifierName(serviceIdentifier: ServiceIdentifier) { + if (typeof serviceIdentifier === 'function' && isType(serviceIdentifier)) { + return serviceIdentifier.name; + } else { + return serviceIdentifier.toString(); + } +} + +export function isType(v: any): v is Type { + return typeof v === 'function' && 'prototype' in v; +} + +export function isTypeProvider(v: Provider): v is TypeProvider { + return isType(v); +} + +export function isValueProvider(v: Provider): v is ValueProvider { + return 'useValue' in v; +} + +export function isClassProvider(v: Provider): v is ClassProvider { + return 'useClass' in v && isType(v.useClass); +} + +export function isFactoryProvider(v: Provider): v is FactoryProvider { + return 'useFactory' in v && typeof v.useFactory === 'function'; +} diff --git a/packages/core/src/errors/dependency-provider-not-found.ts b/packages/core/src/errors/dependency-provider-not-found.ts index aebcd583fd..2f73e057e6 100644 --- a/packages/core/src/errors/dependency-provider-not-found.ts +++ b/packages/core/src/errors/dependency-provider-not-found.ts @@ -1,6 +1,4 @@ -import { ServiceIdentifier } from '../di'; - -import { getServiceIdentifierName } from '../utils'; +import { ServiceIdentifier, getServiceIdentifierName } from '../di'; export class DependencyProviderNotFoundError extends Error { constructor(private _dependency: ServiceIdentifier, private _dependent: ServiceIdentifier, private _moduleName: string) { diff --git a/packages/core/src/errors/provider-already-defined.ts b/packages/core/src/errors/provider-already-defined.ts index 16b38efeaf..f8ab0690c5 100644 --- a/packages/core/src/errors/provider-already-defined.ts +++ b/packages/core/src/errors/provider-already-defined.ts @@ -1,5 +1,4 @@ -import { ServiceIdentifier } from '../di/types'; -import { getServiceIdentifierName } from '../utils'; +import { ServiceIdentifier, getServiceIdentifierName } from '../di'; export class ProviderAlreadyDefinedError extends Error { constructor(private _moduleName: string, private _serviceIdentifier: ServiceIdentifier) { diff --git a/packages/core/src/errors/provider-not-valid.ts b/packages/core/src/errors/provider-not-valid.ts index 0dd07b2ae8..4cca9bd32b 100644 --- a/packages/core/src/errors/provider-not-valid.ts +++ b/packages/core/src/errors/provider-not-valid.ts @@ -1,5 +1,4 @@ -import { ServiceIdentifier } from '../di/types'; -import { getServiceIdentifierName } from '../utils'; +import { ServiceIdentifier, getServiceIdentifierName } from '../di'; export class ProviderNotValidError extends Error { constructor(private _moduleName: string, private _serviceIdentifier: ServiceIdentifier) { diff --git a/packages/core/src/errors/service-identifier-not-found.ts b/packages/core/src/errors/service-identifier-not-found.ts index fb5057cd9b..65ac82cad0 100644 --- a/packages/core/src/errors/service-identifier-not-found.ts +++ b/packages/core/src/errors/service-identifier-not-found.ts @@ -1,5 +1,4 @@ -import { getServiceIdentifierName } from '../utils'; -import { ServiceIdentifier } from '../di'; +import { ServiceIdentifier, getServiceIdentifierName } from '../di'; export class ServiceIdentifierNotFoundError extends Error { constructor(protected _serviceIdentifier: ServiceIdentifier, private _dependent: string) { diff --git a/packages/core/src/graphql-module.ts b/packages/core/src/graphql-module.ts index 999d043b2e..3d59318557 100644 --- a/packages/core/src/graphql-module.ts +++ b/packages/core/src/graphql-module.ts @@ -2,10 +2,11 @@ import { IResolvers, makeExecutableSchema, SchemaDirectiveVisitor } from 'graphq import { mergeGraphQLSchemas, mergeResolvers } from '@graphql-modules/epoxy'; import { Provider, ModuleContext, Injector } from './di'; import { DocumentNode, print, GraphQLSchema } from 'graphql'; -import { IResolversComposerMapping, composeResolvers, asArray } from './resolvers-composition'; +import { IResolversComposerMapping, composeResolvers } from './resolvers-composition'; import { DepGraph } from 'dependency-graph'; import { DependencyModuleNotFoundError, SchemaNotValidError, DependencyModuleUndefinedError, TypeDefNotFoundError } from './errors'; import deepmerge = require('deepmerge'); +import { addInjectorToResolversContext, addInjectorToResolversCompositionContext } from './utils'; /** * A context builder method signature for `contextBuilder`. @@ -27,6 +28,8 @@ export type ModulesMap = Map>; */ export type ModuleDependency = GraphQLModule | string; +export type GraphQLModuleOption = Option | ((module: GraphQLModule) => Option); + /** * Defined the structure of GraphQL module options object. */ @@ -42,12 +45,12 @@ 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?: string | string[] | DocumentNode | DocumentNode[] | ((config: Config, module: GraphQLModule) => string | string[] | DocumentNode | DocumentNode[]); + typeDefs?: GraphQLModuleOption; /** * Resolvers object, or a function will get the module's config as argument, and should * return the resolvers object. */ - resolvers?: IResolvers | ((config: Config, module: GraphQLModule) => IResolvers); + resolvers?: GraphQLModuleOption; /** * Context builder method. Use this to add your own fields and data to the GraphQL `context` * of each execution of GraphQL. @@ -59,17 +62,17 @@ export interface GraphQLModuleOptions { * Adding a dependency will effect the order of the type definition building, resolvers building and context * building. */ - imports?: ((config: Config, module: GraphQLModule) => Array> | string[]) | string[] | Array>; + imports?: GraphQLModuleOption>, Config, Request, Context>; /** * A list of `Providers` to load into the GraphQL module. * It could be either a `class` or a value/class instance. * All loaded class will be loaded as Singletons, and the instance will be * shared across all GraphQL executions. */ - providers?: Provider[] | ((config: Config, module: GraphQLModule) => Provider[]); + providers?: GraphQLModuleOption; /** Object map between `Type.field` to a function(s) that will wrap the resolver of the field */ - resolversComposition?: IResolversComposerMapping | ((config: Config, module: GraphQLModule) => IResolversComposerMapping); - schemaDirectives?: ISchemaDirectives | ((config: Config, module: GraphQLModule) => ISchemaDirectives); + resolversComposition?: GraphQLModuleOption; + schemaDirectives?: GraphQLModuleOption; } /** @@ -117,10 +120,9 @@ export class GraphQLModule { * @param options - module configuration */ constructor( - private _options: GraphQLModuleOptions, + private _options: GraphQLModuleOptions = {}, private _moduleConfig: Config = {} as Config, ) { - _options = _options || {}; _options.name = _options.name || Math.floor(Math.random() * Math.floor(Number.MAX_SAFE_INTEGER)).toString(); } @@ -217,7 +219,7 @@ export class GraphQLModule { const typeDefsDefinitions = this._options.typeDefs; if (typeDefsDefinitions) { if (typeof typeDefsDefinitions === 'function') { - typeDefs = typeDefsDefinitions(this._moduleConfig, this); + typeDefs = typeDefsDefinitions(this); } else if (Array.isArray(typeDefsDefinitions)) { typeDefs = mergeGraphQLSchemas(typeDefsDefinitions); } else if (typeof typeDefsDefinitions === 'string') { @@ -234,7 +236,7 @@ export class GraphQLModule { const resolversDefinitions = this._options.resolvers; if (resolversDefinitions) { if (typeof resolversDefinitions === 'function') { - resolvers = resolversDefinitions(this._moduleConfig, this); + resolvers = resolversDefinitions(this); } else { resolvers = resolversDefinitions; } @@ -246,7 +248,7 @@ export class GraphQLModule { let imports = new Array>(); if (this._options.imports) { if (typeof this._options.imports === 'function') { - imports = this._options.imports(this._moduleConfig, this); + imports = this._options.imports(this); } else { imports = this._options.imports; } @@ -259,7 +261,7 @@ export class GraphQLModule { const providersDefinitions = this._options.providers; if (providersDefinitions) { if (typeof providersDefinitions === 'function') { - providers = providersDefinitions(this._moduleConfig, this); + providers = providersDefinitions(this); } else { providers = providersDefinitions; } @@ -278,7 +280,7 @@ export class GraphQLModule { const resolversCompositionDefinitions = this._options.resolversComposition; if (resolversCompositionDefinitions) { if (typeof resolversCompositionDefinitions === 'function') { - resolversComposition = (resolversCompositionDefinitions as any)(this._moduleConfig); + resolversComposition = (resolversCompositionDefinitions as any)(this); } else { resolversComposition = resolversCompositionDefinitions; } @@ -286,27 +288,12 @@ export class GraphQLModule { return resolversComposition; } - private wrapResolversComposition(resolversComposition: IResolversComposerMapping) { - // tslint:disable-next-line:forin - for (const path in resolversComposition) { - const compositionArr = asArray(resolversComposition[path]); - resolversComposition[path] = [ - (next: any) => (root: any, args: any, context: any, info: any) => next(root, args, { - ...context, - injector: this._cache.injector, - }, info), - ...compositionArr, - ]; - } - return resolversComposition; - } - get selfSchemaDirectives(): ISchemaDirectives { let schemaDirectives: ISchemaDirectives = {}; const schemaDirectivesDefinitions = this._options.schemaDirectives; if (schemaDirectivesDefinitions) { if (typeof schemaDirectivesDefinitions === 'function') { - schemaDirectives = schemaDirectivesDefinitions(this._moduleConfig, this); + schemaDirectives = schemaDirectivesDefinitions(this); } else { schemaDirectives = schemaDirectivesDefinitions; } @@ -330,12 +317,14 @@ export class GraphQLModule { module.buildSchemaAndInjector(modulesMap); } - const typeDefs = module._cache.typeDefs; - const resolvers = module._cache.resolvers; const injector = module._cache.injector; + const resolvers = module._cache.resolvers; + const typeDefs = module._cache.typeDefs; const contextBuilder = module._cache.contextBuilder; const schemaDirectives = module._cache.schemaDirectives; + importsInjectors.add(injector); + importsResolvers.add(resolvers); if (typeDefs && typeDefs.length) { if (Array.isArray(typeDefs)) { for (const typeDef of typeDefs) { @@ -345,9 +334,6 @@ export class GraphQLModule { importsTypeDefs.add(typeDefs); } } - - importsResolvers.add(resolvers); - importsInjectors.add(injector); importsContextBuilders.add(contextBuilder); importsSchemaDirectives.add(schemaDirectives); } @@ -366,28 +352,9 @@ export class GraphQLModule { this._cache.injector = injector; - const resolvers = this.selfResolvers; - // tslint:disable-next-line:forin - for ( const type in resolvers ) { - const typeResolvers = resolvers[type]; - // tslint:disable-next-line:forin - for (const prop in resolvers[type]) { - const resolver = typeResolvers[prop]; - if (typeof resolver === 'function') { - if (prop !== '__resolveType') { - typeResolvers[prop] = (root: any, args: any, context: any, info: any) => { - return resolver.call(typeResolvers, root, args, { injector, ...context }, info); - }; - } else { - typeResolvers[prop] = (root: any, context: any, info: any) => { - return resolver.call(typeResolvers, root, { injector, ...context }, info); - }; - } - } - } - } + const resolvers = addInjectorToResolversContext(this.selfResolvers, injector); - const resolversComposition = this.wrapResolversComposition(this.selfResolversComposition); + const resolversComposition = addInjectorToResolversCompositionContext(this.selfResolversComposition, injector); const resolversToBeComposed = new Set(importsResolvers); resolversToBeComposed.add(resolvers); @@ -586,7 +553,7 @@ export class GraphQLModule { // if it is merged module, get one module, it will be enough to get merged one. return modulesMap.get(moduleName); }); - const mergedModule = GraphQLModule.mergeModules(circularModules, modulesMap); + const mergedModule = GraphQLModule.mergeModules(circularModules, modulesMap); for (const moduleName of realPath) { modulesMap.set(moduleName, mergedModule); for (const subModuleName of moduleName.split('+')) { diff --git a/packages/core/src/resolvers-composition.ts b/packages/core/src/resolvers-composition.ts index c9443e2971..d76823e040 100644 --- a/packages/core/src/resolvers-composition.ts +++ b/packages/core/src/resolvers-composition.ts @@ -1,5 +1,6 @@ import { get, set } from 'lodash'; import { IResolvers } from 'graphql-tools'; +import { chainFunctions, asArray } from './utils'; export interface IResolversComposerMapping { [resolverPath: string]: any | any[]; @@ -25,16 +26,6 @@ function resolveRelevantMappings(resolvers: IResolvers, path: string, allMapping return result; } -export const asArray = (fns: T | T[]) => (Array.isArray(fns) ? fns : [fns]); - -export function chainFunctions(funcs: any[]) { - if (funcs.length === 1) { - return funcs[0]; - } - - return funcs.reduce((a, b) => (...args: any[]) => a(b(...args))); -} - /** * Wraps the resolvers object with the resolvers composition objects. * Implemented as a simple and basic middleware mechanism. diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 4924fd192b..6674bd840a 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -1,31 +1,51 @@ -import { ServiceIdentifier, Provider, Type, ValueProvider, ClassProvider, FactoryProvider, TypeProvider } from './di/types'; +import { IResolversComposerMapping } from './resolvers-composition'; +import { IResolvers } from 'graphql-tools'; +import { Injector } from './di'; -export const DESIGN_PARAM_TYPES = 'design:paramtypes'; +export const asArray = (fns: T | T[]) => (Array.isArray(fns) ? fns : [fns]); -export function getServiceIdentifierName(serviceIdentifier: ServiceIdentifier) { - if (typeof serviceIdentifier === 'function' && isType(serviceIdentifier)) { - return serviceIdentifier.name; - } else { - return serviceIdentifier.toString(); +export function chainFunctions(funcs: any[]) { + if (funcs.length === 1) { + return funcs[0]; } -} - -export function isType(v: any): v is Type { - return typeof v === 'function' && 'prototype' in v; -} -export function isTypeProvider(v: Provider): v is TypeProvider { - return isType(v); + return funcs.reduce((a, b) => (...args: any[]) => a(b(...args))); } -export function isValueProvider(v: Provider): v is ValueProvider { - return 'useValue' in v; +export function addInjectorToResolversContext(resolvers: IResolvers, injector: Injector) { + // tslint:disable-next-line:forin + for ( const type in resolvers ) { + const typeResolvers = resolvers[type]; + // tslint:disable-next-line:forin + for (const prop in resolvers[type]) { + const resolver = typeResolvers[prop]; + if (typeof resolver === 'function') { + if (prop !== '__resolveType') { + typeResolvers[prop] = (root: any, args: any, context: any, info: any) => { + return resolver.call(typeResolvers, root, args, { injector, ...context }, info); + }; + } else { + typeResolvers[prop] = (root: any, context: any, info: any) => { + return resolver.call(typeResolvers, root, { injector, ...context }, info); + }; + } + } + } + } + return resolvers; } -export function isClassProvider(v: Provider): v is ClassProvider { - return 'useClass' in v && isType(v.useClass); -} - -export function isFactoryProvider(v: Provider): v is FactoryProvider { - return 'useFactory' in v && typeof v.useFactory === 'function'; +export function addInjectorToResolversCompositionContext(resolversComposition: IResolversComposerMapping, injector: Injector) { + // tslint:disable-next-line:forin + for (const path in resolversComposition) { + const compositionArr = asArray(resolversComposition[path]); + resolversComposition[path] = [ + (next: any) => (root: any, args: any, context: any, info: any) => next(root, args, { + ...context, + injector, + }, info), + ...compositionArr, + ]; + } + return resolversComposition; }