Skip to content

Commit

Permalink
Fix for Providers that don't provide classType
Browse files Browse the repository at this point in the history
- Updated the API to differentiate between the constructor of the
injectable vs the constructor of the dependency

- Add better checking and reflection to make sure we don't reflect
on undefined objects
  • Loading branch information
WonderPanda committed Mar 6, 2019
1 parent f8ccf16 commit ebb296b
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 95 deletions.
22 changes: 22 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
(The MIT License)

Copyright (c) 2019 Jesse Carter

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 changes: 9 additions & 2 deletions packages/discovery/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,22 @@ In the case of querying for `providers` or `controllers`, the service returns th
export interface DiscoveredModule {
name: string;
instance: {};
classType: Type<{}>;
injectType?: Type<{}>;
dependencyType: Type<{}>;
}

export interface DiscoveredClass extends DiscoveredModule {
parentModule: DiscoveredModule;
}
```

This gives access to the (singleton) `instance` of the matching provider or controller created by the NestJS Dependency Injection container as well as the `classType` which is the class constructor function. It also provides the string based name for convenience. A `DiscoveredClass` contains a `parentModule` which provides the same set of information for the `@Module` class that the dependency was discovered in.
This gives access to the (singleton) `instance` of the matching provider or controller created by the NestJS Dependency Injection container.

The `injectType` can contain the constructor function of the provider token if it is provided as an @Injectable class. In the case of custom providers, this value will either contain the type of the factory function that created the dependency, or undefined if a value was directly provided with `useValue`.

The `dependencyType` is a shortcut to retrieve the constructor function of the actual provided dependency itself. For @Injectable providers/controllers this will simply be the decorated class but for dyanmic providers it will return the constructor function of whatever dependency was actually returned from `useValue` or `useFactory`.

It also provides the string based name for convenience. A `DiscoveredClass` contains a `parentModule` which provides the same set of information for the `@Module` class that the dependency was discovered in.

When querying for methods on `providers` or `controllers` the following interface is returned:

Expand Down
3 changes: 2 additions & 1 deletion packages/discovery/src/discovery.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { Type } from '@nestjs/common';
export interface DiscoveredModule {
name: string;
instance: {};
classType: Type<{}>;
injectType?: Type<{}>;
dependencyType: Type<{}>;
}

export interface DiscoveredClass extends DiscoveredModule {
Expand Down
47 changes: 37 additions & 10 deletions packages/discovery/src/discovery.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { InstanceWrapper } from '@nestjs/core/injector/container';
import { Module } from '@nestjs/core/injector/module';
import { ModulesContainer } from '@nestjs/core/injector/modules-container';
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import { flatMap, uniqBy } from 'lodash';
import { flatMap, some, uniqBy } from 'lodash';
import {
DiscoveredClass,
DiscoveredClassWithMeta,
Expand All @@ -13,16 +13,43 @@ import {
MetaKey
} from './discovery.interfaces';

/**
* Attempts to retrieve meta information from a Nest DiscoveredClass component
* @param key The meta key to retrieve data from
* @param component The discovered component to retrieve meta from
*/
export function getComponentMetaAtKey<T>(
key: MetaKey,
component: DiscoveredClass
): T | undefined {
const dependencyMeta = Reflect.getMetadata(
key,
component.dependencyType
) as T;
if (dependencyMeta) {
return dependencyMeta;
}

if (component.injectType != null) {
return Reflect.getMetadata(key, component.injectType) as T;
}
}

/**
* A filter that can be used to search for DiscoveredClasses in an App that contain meta attached to a
* certain key
* @param key The meta key to search for
*/
export const withMetaAtKey: (
key: MetaKey
) => Filter<DiscoveredClass> = key => component =>
Reflect.getMetadata(key, component.classType) ||
Reflect.getMetadata(key, component.instance.constructor);
) => Filter<DiscoveredClass> = key => component => {
const metaTargets: Function[] = [
component.instance.constructor,
component.injectType as Function
].filter(x => x != null);

return some(metaTargets, x => Reflect.getMetadata(key, x));
};

@Injectable()
export class DiscoveryService {
Expand Down Expand Up @@ -106,9 +133,7 @@ export class DiscoveryService {
const providers = await this.providers(withMetaAtKey(metaKey));

return providers.map(x => ({
meta:
(Reflect.getMetadata(metaKey, x.classType) as T) ||
Reflect.getMetadata(metaKey, x.instance.constructor),
meta: getComponentMetaAtKey<T>(metaKey, x) as T,
discoveredClass: x
}));
}
Expand All @@ -133,7 +158,7 @@ export class DiscoveryService {
const controllers = await this.controllers(withMetaAtKey(metaKey));

return controllers.map(x => ({
meta: Reflect.getMetadata(metaKey, x.classType) as T,
meta: getComponentMetaAtKey<T>(metaKey, x) as T,
discoveredClass: x
}));
}
Expand Down Expand Up @@ -203,11 +228,13 @@ export class DiscoveryService {
return {
name: component.name as string,
instance: component.instance,
classType: component.metatype,
injectType: component.metatype,
dependencyType: component.instance.constructor,
parentModule: {
name: nestModule.metatype.name,
instance: nestModule.instance,
classType: nestModule.metatype
injectType: nestModule.metatype,
dependencyType: component.instance.constructor
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { METHOD_METADATA, PATH_METADATA } from '@nestjs/common/constants';
import { Test, TestingModule } from '@nestjs/testing';
import { DiscoveryModule, DiscoveryService } from '..';
import { getComponentMetaAtKey } from '../discovery.service';

const rolesKey = 'roles';
const Roles = (roles: string[]) => ReflectMetadata(rolesKey, roles);
Expand Down Expand Up @@ -86,7 +87,7 @@ describe('Advanced Controller Discovery', () => {

expect(guestControllers).toHaveLength(1);
const [guestController] = guestControllers;
expect(guestController.discoveredClass.classType).toBe(GuestController);
expect(guestController.discoveredClass.injectType).toBe(GuestController);
expect(guestController.discoveredClass.instance).toBeInstanceOf(
GuestController
);
Expand All @@ -111,9 +112,9 @@ describe('Advanced Controller Discovery', () => {
expect(allMethods).toHaveLength(4);

const fullPaths = allMethods.map(x => {
const controllerPath = Reflect.getMetadata(
const controllerPath = getComponentMetaAtKey<string>(
PATH_METADATA,
x.discoveredMethod.parentClass.classType
x.discoveredMethod.parentClass
);

const methodPath = Reflect.getMetadata(
Expand Down
75 changes: 0 additions & 75 deletions packages/discovery/src/tests/async-providers.spec.ts

This file was deleted.

10 changes: 6 additions & 4 deletions packages/discovery/src/tests/discovery.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('Discovery', () => {

expect(providers).toHaveLength(1);
const [provider] = providers;
expect(provider.classType).toBe(ExampleService);
expect(provider.injectType).toBe(ExampleService);
expect(provider.instance).toBeInstanceOf(ExampleService);
});

Expand All @@ -85,7 +85,8 @@ describe('Discovery', () => {
discoveredMethod: {
methodName: 'specialMethod',
parentClass: {
classType: ExampleService
injectType: ExampleService,
dependencyType: ExampleService
}
}
});
Expand All @@ -104,7 +105,7 @@ describe('Discovery', () => {

expect(controllers).toHaveLength(1);
const [controller] = controllers;
expect(controller.classType).toBe(ExampleController);
expect(controller.injectType).toBe(ExampleController);
expect(controller.instance).toBeInstanceOf(ExampleController);
});

Expand All @@ -123,7 +124,8 @@ describe('Discovery', () => {
discoveredMethod: {
methodName: 'get',
parentClass: {
classType: ExampleController
injectType: ExampleController,
dependencyType: ExampleController
}
}
});
Expand Down
Loading

0 comments on commit ebb296b

Please sign in to comment.