Skip to content

Commit

Permalink
Enhance errors and introduce providedIn
Browse files Browse the repository at this point in the history
Signed-off-by: Arda TANRIKULU <ardatanrikulu@gmail.com>
  • Loading branch information
ardatan committed Nov 8, 2018
1 parent 654cf45 commit 39021d6
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 72 deletions.
12 changes: 10 additions & 2 deletions packages/core/src/di/injectable.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { DESIGN_PARAM_TYPES } from '../utils';
import { DESIGN_PARAM_TYPES, INJECTABLE_OPTIONS } from '../utils';
import { GraphQLModule } from '../graphql-module';

declare var Reflect: any;

export function Injectable() {
export interface InjectableOptions {
providedIn?: string | GraphQLModule;
}

export function Injectable(options ?: InjectableOptions) {
return (target: any) => {
if (options) {
Reflect.defineMetadata(INJECTABLE_OPTIONS, options, target);
}
if (!Reflect.hasMetadata(DESIGN_PARAM_TYPES, target)) {
Reflect.defineMetadata(DESIGN_PARAM_TYPES, [], target);
}
Expand Down
128 changes: 74 additions & 54 deletions packages/core/src/di/injector.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,91 @@
import { Provider, ServiceIdentifier, Factory, OnRequest } from './types';
import { isType, isValue, isClass, isFactory, DESIGN_PARAM_TYPES } from '../utils';
import { isType, DESIGN_PARAM_TYPES, isValueProvider, isClassProvider, isFactoryProvider, isTypeProvider, INJECTABLE_OPTIONS } from '../utils';
import { GraphQLModule } from '../graphql-module';
import { ServiceIdentifierNotFoundError, DependencyProviderNotFoundError } from '../errors';
import { ServiceIdentifierNotFoundError, DependencyProviderNotFoundError, ProviderNotValidError } from '../errors';

declare var Reflect: any;

export class Injector {
children = new Array<Injector>();
types = new Array<any>();
valueMap = new Map();
classMap = new Map();
factoryMap = new Map();
instanceMap = new Map();
public children = new Array<Injector>();
private _types = new Set<any>();
private _valueMap = new Map();
private _classMap = new Map();
private _factoryMap = new Map();
private _instanceMap = new Map();
constructor(public moduleName: string) {}
public provide<T>(provider: Provider<T>): void {
if (Array.isArray(provider)) {
return provider.forEach(p => this.provide(p));
}
if (isType(provider)) {
this.types.push(provider);
} else if (isValue(provider)) {
this.valueMap.set(provider.provide, provider.useValue);
} else if (isClass(provider)) {
this.classMap.set(provider.provide, provider.useClass);
} else if (isFactory(provider)) {
this.factoryMap.set(provider.provide, provider.useFactory);
if (isTypeProvider(provider)) {
this._types.add(provider);
} else if (isValueProvider(provider)) {
if (this._valueMap.has(provider.provide) && !provider.overwrite) {
throw new Error(`Provider #`);
}
this._valueMap.set(provider.provide, provider.useValue);
} else if (isClassProvider(provider)) {
this._classMap.set(provider.provide, provider.useClass);
} else if (isFactoryProvider(provider)) {
this._factoryMap.set(provider.provide, provider.useFactory);
} else {
throw new Error(`Couldn't provide ${provider}`);
throw new ProviderNotValidError(this.moduleName, provider['provide'] && JSON.stringify(provider));
}
}
public get<T>(serviceIdentifier: ServiceIdentifier<T>): T {
if (this.types.includes(serviceIdentifier)) {
if (!this.instanceMap.has(serviceIdentifier)) {
this.instanceMap.set(serviceIdentifier, this.instantiate(serviceIdentifier));
}
return this.instanceMap.get(serviceIdentifier);
} else if (this.valueMap.has(serviceIdentifier)) {
return this.valueMap.get(serviceIdentifier);
} else if (this.classMap.has(serviceIdentifier)) {
const realClazz = this.classMap.get(serviceIdentifier);
if (!this.instanceMap.has(realClazz)) {
this.instanceMap.set(realClazz, this.instantiate(realClazz));
}
return this.instanceMap.get(realClazz);
} else if (this.factoryMap.has(serviceIdentifier)) {
if (!this.instanceMap.has(serviceIdentifier)) {
const factory = this.factoryMap.get(serviceIdentifier);
this.instanceMap.set(serviceIdentifier, this.callFactory(factory));
}
return this.instanceMap.get(serviceIdentifier);
} else {
for (const child of this.children) {
try {
return child.get(serviceIdentifier);
} catch (e) {
if (e instanceof ServiceIdentifierNotFoundError) {
continue;
} else {
throw e;
if (this._types.has(serviceIdentifier)) {
if (!this._instanceMap.has(serviceIdentifier)) {
this._instanceMap.set(serviceIdentifier, this.instantiate(serviceIdentifier));
}
return this._instanceMap.get(serviceIdentifier);
} else if (this._valueMap.has(serviceIdentifier)) {
return this._valueMap.get(serviceIdentifier);
} else if (this._classMap.has(serviceIdentifier)) {
const realClazz = this._classMap.get(serviceIdentifier);
if (!this._instanceMap.has(realClazz)) {
this._instanceMap.set(realClazz, this.instantiate(realClazz));
}
return this._instanceMap.get(realClazz);
} else if (this._factoryMap.has(serviceIdentifier)) {
if (!this._instanceMap.has(serviceIdentifier)) {
const factory = this._factoryMap.get(serviceIdentifier);
this._instanceMap.set(serviceIdentifier, this.callFactory(factory));
}
return this._instanceMap.get(serviceIdentifier);
} else {
for (const child of this.children) {
try {
return child.get(serviceIdentifier);
} catch (e) {
if (e instanceof ServiceIdentifierNotFoundError) {
continue;
} else {
throw e;
}
}
}
if (isType(serviceIdentifier) && this.checkInjectableOptions(serviceIdentifier)) {
this._types.add(serviceIdentifier);
const instance = this.instantiate<T>(serviceIdentifier);
this._instanceMap.set(serviceIdentifier, instance);
return instance;
}
throw new ServiceIdentifierNotFoundError(serviceIdentifier, this.moduleName);
}
}

private checkInjectableOptions(clazz: any) {
if (Reflect.hasMetadata(INJECTABLE_OPTIONS, clazz)) {
let { providedIn } = Reflect.getMetadata(INJECTABLE_OPTIONS, clazz);
if (typeof providedIn === 'function') {
providedIn = providedIn();
}
if (typeof providedIn === 'string') {
return providedIn === this.moduleName;
} else if (providedIn instanceof GraphQLModule) {
console.log(providedIn.name, this.moduleName);
return providedIn.name === this.moduleName;
}
throw new ServiceIdentifierNotFoundError(serviceIdentifier);
}
}
return false;
}

public instantiate<T>(clazz: any): T {
try {
Expand All @@ -75,7 +98,7 @@ export class Injector {
return instance;
} catch (e) {
if (e instanceof ServiceIdentifierNotFoundError) {
throw new DependencyProviderNotFoundError(e.serviceIdentifier, clazz);
throw new DependencyProviderNotFoundError(e.serviceIdentifier, clazz, this.moduleName);
} else {
throw e;
}
Expand All @@ -95,9 +118,6 @@ export class Injector {
}

public init<T>(provider: Provider<T>): void {
if (Array.isArray(provider)) {
return provider.forEach(p => this.init(p));
}
this.getByProvider(provider);
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/di/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface FactoryProvider<T> extends BaseProvider<T> {

export interface BaseProvider<T> {
provide: ServiceIdentifier<T>;
overwrite?: boolean;
}

export interface TypeProvider<T> extends Type<T> {}
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/errors/dependency-provider-not-found.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { ServiceIdentifier } from '../di';
import { getServiceIdentifierName } from '../utils';

export class DependencyProviderNotFoundError<Dependency, Dependent> extends Error {
constructor(private _dependency: ServiceIdentifier<Dependency>, private _dependent: ServiceIdentifier<Dependent>) {
constructor(private _dependency: ServiceIdentifier<Dependency>, private _dependent: ServiceIdentifier<Dependent>, private _moduleName: string) {
super(`
GraphQL-Modules Error: Dependency Provider Not Found!
- Provider #${getServiceIdentifierName(_dependency)} couldn't be injected into Provider #${getServiceIdentifierName(_dependent)}
-- Provider #${getServiceIdentifierName(_dependency)} is not provided in that scope!
-- Provider #${getServiceIdentifierName(_dependency)} is not provided in #Module ${_moduleName} scope!
Possible solutions:
- Make sure you have imported the module of Provider #${getServiceIdentifierName(_dependency)} in the module of Provider #${getServiceIdentifierName(_dependent)}
Expand All @@ -23,4 +23,8 @@ export class DependencyProviderNotFoundError<Dependency, Dependent> extends Erro
get dependent(): ServiceIdentifier<Dependent> {
return this._dependent;
}

get moduleName(): string {
return this._moduleName;
}
}
1 change: 1 addition & 0 deletions packages/core/src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { DependencyModuleNotFoundError } from './dependency-module-not-found';
export { SchemaNotValidError } from './schema-not-valid';
export { DependencyModuleUndefinedError } from './dependency-module-undefined';
export { TypeDefNotFoundError } from './typedef-not-found';
export { ProviderNotValidError } from './provider-not-valid';
23 changes: 23 additions & 0 deletions packages/core/src/errors/provider-already-defined.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ServiceIdentifier } from '../di/types';
import { getServiceIdentifierName } from '../utils';

export class ProviderAlreadyDefinedError<T> extends Error {
constructor(private _moduleName: string, private _serviceIdentifier: ServiceIdentifier<T>) {
super(`
GraphQL-Modules Error: Provider has been already defined!
- #Module #${_moduleName} already has a #Provider #${getServiceIdentifierName(_serviceIdentifier)}.
Possible solutions:
- Provider must have 'override: true' field.
`);
Object.setPrototypeOf(this, ProviderAlreadyDefinedError.prototype);
Error.captureStackTrace(this, ProviderAlreadyDefinedError);
}
get moduleName() {
return this._moduleName;
}

get serviceIdentifier(): ServiceIdentifier<T> {
return this._serviceIdentifier;
}
}
24 changes: 24 additions & 0 deletions packages/core/src/errors/provider-not-valid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ServiceIdentifier } from '../di/types';
import { getServiceIdentifierName } from '../utils';

export class ProviderNotValidError<T> extends Error {
constructor(private _moduleName: string, private _serviceIdentifier: ServiceIdentifier<T>) {
super(`
GraphQL-Modules Error: Provider is not valid!
- #Module #${_moduleName} provides an invalid #Provider #${getServiceIdentifierName(_serviceIdentifier)}!
Possible solutions:
- Provider must be a class itself,
or provides a valid identifier with 'useValue', 'useFactory' or 'useClass'.
`);
Object.setPrototypeOf(this, ProviderNotValidError.prototype);
Error.captureStackTrace(this, ProviderNotValidError);
}
get moduleName() {
return this._moduleName;
}

get serviceIdentifier(): ServiceIdentifier<T> {
return this._serviceIdentifier;
}
}
2 changes: 1 addition & 1 deletion packages/core/src/errors/schema-not-valid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export class SchemaNotValidError extends Error {
constructor(moduleName: string, error: string) {
super(`
GraphQL-Modules Error: Schema is not valid!
- Module #${moduleName} doesn't have a valid schema!
- #Module #${moduleName} doesn't have a valid schema!
-- ${error}
Possible solutions:
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/errors/service-identifier-not-found.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { getServiceIdentifierName } from '../utils';
import { ServiceIdentifier } from '../di';

export class ServiceIdentifierNotFoundError<T> extends Error {
constructor(protected _serviceIdentifier: ServiceIdentifier<T>) {
constructor(protected _serviceIdentifier: ServiceIdentifier<T>, private _dependent: string) {
super(`
GraphQL-Modules Error: Dependency Provider Not Found!
- Provider #${getServiceIdentifierName(_serviceIdentifier)} not provided in that scope!
- Provider #${getServiceIdentifierName(_serviceIdentifier)} not provided in #Module ${_dependent} scope!
Possible solutions:
- Check if you have this provider in your module.
Expand All @@ -15,6 +15,10 @@ export class ServiceIdentifierNotFoundError<T> extends Error {
Error.captureStackTrace(this, ServiceIdentifierNotFoundError);
}

get dependent() {
return this._dependent;
}

get serviceIdentifier(): ServiceIdentifier<T> {
return this._serviceIdentifier;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/graphql-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ export class GraphQLModule<Config = any, Request = any, Context = any> {
importsSchemaDirectives = { ...importsSchemaDirectives, ...schemaDirectives };
}

const injector = new Injector();
const injector = new Injector(this.name);
injector.children = importsInjectors;

const providers = this.selfProviders;
Expand Down
27 changes: 18 additions & 9 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import { ServiceIdentifier, Provider, Type, ValueProvider, ClassProvider, FactoryProvider } from './di/types';
import { ServiceIdentifier, Provider, Type, ValueProvider, ClassProvider, FactoryProvider, TypeProvider } from './di/types';

export const DESIGN_PARAM_TYPES = 'design:paramtypes';
export const INJECTABLE_OPTIONS = 'injectable:options';

export function getServiceIdentifierName<T>(serviceIdentifier: ServiceIdentifier<T>) {
return serviceIdentifier['name'] || serviceIdentifier.toString();
if (typeof serviceIdentifier === 'function' && isType<T>(serviceIdentifier)) {
return serviceIdentifier.name;
} else {
return serviceIdentifier.toString();
}
}

export function isType<T>(v: Provider<T>): v is Type<T> {
return typeof v === 'function';
export function isType<T>(v: any): v is Type<T> {
return typeof v === 'function' && 'prototype' in v;
}

export function isValue<T>(v: Provider<T>): v is ValueProvider<T> {
export function isTypeProvider<T>(v: Provider<T>): v is TypeProvider<T> {
return isType<T>(v);
}

export function isValueProvider<T>(v: Provider<T>): v is ValueProvider<T> {
return 'useValue' in v;
}

export function isClass<T>(v: Provider<T>): v is ClassProvider<T> {
return 'useClass' in v;
export function isClassProvider<T>(v: Provider<T>): v is ClassProvider<T> {
return 'useClass' in v && isType(v.useClass);
}

export function isFactory<T>(v: Provider<T>): v is FactoryProvider<T> {
return 'useFactory' in v;
export function isFactoryProvider<T>(v: Provider<T>): v is FactoryProvider<T> {
return 'useFactory' in v && typeof v.useFactory === 'function';
}
11 changes: 10 additions & 1 deletion packages/core/tests/graphql-module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,15 @@ describe('GraphQLModule', () => {
expect(app.schema).toBeDefined();
});

it('should handle providedIn', async () => {
const module = new GraphQLModule({});
@Injectable({
providedIn: module,
})
class ProviderA {}
expect(module.injector.get(ProviderA) instanceof ProviderA).toBeTruthy();
});

describe('Schema merging', () => {
it('should merge types and directives correctly', async () => {
const m1 = new GraphQLModule({
Expand Down Expand Up @@ -391,7 +400,7 @@ describe('GraphQLModule', () => {
contextValue,
});
expect(result.data.test).toBeNull();
expect(result.errors[0].message).toContain('ProviderB not provided in that scope!');
expect(result.errors[0].message).toContain('ProviderB not provided in');
});
});
describe('CommuncationBridge', async () => {
Expand Down

0 comments on commit 39021d6

Please sign in to comment.