mirror of
https://github.com/nestjs/nest.git
synced 2026-10-28 09:45:43 +08:00
fix(core): name the module when metadata entries are invalid (#17819)
* fix(core): name the module when metadata entries are invalid A custom provider without a usable useClass, useValue, useFactory or useExisting, and an undefined entry in controllers or exports, crashed bootstrap with a bare TypeError naming neither the module nor the token. Throw a descriptive error instead, as undefined imports and providers already do. * test(core): cover the injectable path and the invalid provider message Pin the paths the previous commit changed: a custom injectable without a valid "use*" property, the message text for string, symbol and class tokens, and null as well as undefined in the two container specs.
This commit is contained in:
1 parent
a1080277c2
commit
fb8e0d6529
7 files changed
+139
-1
No files matched your search
@@ -0,0 +1,9 @@
|
||||
import { InjectorDependency } from '../../injector/injector.js';
|
||||
import { INVALID_PROVIDER_MESSAGE } from '../messages.js';
|
||||
import { RuntimeException } from './runtime.exception.js';
|
||||
|
||||
export class InvalidProviderException extends RuntimeException {
|
||||
constructor(token: InjectorDependency, moduleName: string) {
|
||||
super(INVALID_PROVIDER_MESSAGE(token, moduleName));
|
||||
}
|
||||
}
|
||||
@@ -224,6 +224,16 @@ Potential causes:
|
||||
Scope [${stringifyScope(scope)}]`;
|
||||
};
|
||||
|
||||
export const INVALID_PROVIDER_MESSAGE = (
|
||||
token: InjectorDependency,
|
||||
moduleName: string,
|
||||
) => `Nest cannot create the ${moduleName} instance.
|
||||
The provider ${getDependencyName(token, 'provider')} in the ${moduleName} "providers" array does not define a valid "useClass", "useValue", "useFactory" or "useExisting" property.
|
||||
|
||||
Potential causes:
|
||||
- The value of "useClass", "useFactory" or "useExisting" is undefined at runtime, often because of a circular import between files. Check your import statements.
|
||||
- None of these properties is set, or the one that is set is null.`;
|
||||
|
||||
export const UNKNOWN_EXPORT_MESSAGE = (
|
||||
token: string | symbol = 'item',
|
||||
module: string,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type EnhancerSubtype,
|
||||
GLOBAL_MODULE_METADATA,
|
||||
type Injectable,
|
||||
isNil,
|
||||
type NestApplicationContextOptions,
|
||||
} from '@nestjs/common/internal';
|
||||
import type { Type } from '@nestjs/common';
|
||||
@@ -291,6 +292,9 @@ export class NestContainer {
|
||||
throw new UnknownModuleException();
|
||||
}
|
||||
const moduleRef = this.modules.get(token)!;
|
||||
if (isNil(toExport)) {
|
||||
throw new CircularDependencyException(moduleRef.metatype.name);
|
||||
}
|
||||
moduleRef.addExportedProviderOrModule(toExport);
|
||||
}
|
||||
|
||||
@@ -299,6 +303,9 @@ export class NestContainer {
|
||||
throw new UnknownModuleException();
|
||||
}
|
||||
const moduleRef = this.modules.get(token)!;
|
||||
if (isNil(controller)) {
|
||||
throw new CircularDependencyException(moduleRef.metatype.name);
|
||||
}
|
||||
moduleRef.addController(controller);
|
||||
|
||||
const controllerRef = moduleRef.controllers.get(controller)!;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
RuntimeException,
|
||||
UnknownExportException,
|
||||
} from '../errors/exceptions/index.js';
|
||||
import { InvalidProviderException } from '../errors/exceptions/invalid-provider.exception.js';
|
||||
import { createContextId } from '../helpers/context-id-factory.js';
|
||||
import { getClassScope } from '../helpers/get-class-scope.js';
|
||||
import { isDurable } from '../helpers/is-durable.js';
|
||||
@@ -318,6 +319,11 @@ export class Module {
|
||||
this.addCustomFactory(provider, collection, enhancerSubtype);
|
||||
} else if (this.isCustomUseExisting(provider)) {
|
||||
this.addCustomUseExisting(provider, collection, enhancerSubtype);
|
||||
} else {
|
||||
throw new InvalidProviderException(
|
||||
(provider as ClassProvider).provide,
|
||||
this.name,
|
||||
);
|
||||
}
|
||||
return provider.provide;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { UnknownDependenciesException } from '../../../errors/exceptions/unknown-dependencies.exception.js';
|
||||
import {
|
||||
INVALID_MODULE_MESSAGE,
|
||||
INVALID_PROVIDER_MESSAGE,
|
||||
UNDEFINED_MODULE_MESSAGE,
|
||||
UNKNOWN_EXPORT_MESSAGE,
|
||||
USING_INVALID_CLASS_AS_A_MODULE_MESSAGE,
|
||||
@@ -383,6 +384,42 @@ Scope [AppModule -> CatsModule]`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('INVALID_PROVIDER_MESSAGE', () => {
|
||||
const expectedMessage = (token: string) =>
|
||||
stringCleaner(`Nest cannot create the AppModule instance.
|
||||
The provider ${token} in the AppModule "providers" array does not define a valid "useClass", "useValue", "useFactory" or "useExisting" property.
|
||||
|
||||
Potential causes:
|
||||
- The value of "useClass", "useFactory" or "useExisting" is undefined at runtime, often because of a circular import between files. Check your import statements.
|
||||
- None of these properties is set, or the one that is set is null.`);
|
||||
|
||||
it('should display a string token', () => {
|
||||
const actualMessage = stringCleaner(
|
||||
INVALID_PROVIDER_MESSAGE('CATS_SERVICE', 'AppModule'),
|
||||
);
|
||||
|
||||
expect(actualMessage).toBe(expectedMessage('"CATS_SERVICE"'));
|
||||
});
|
||||
|
||||
it('should display a symbol token', () => {
|
||||
const actualMessage = stringCleaner(
|
||||
INVALID_PROVIDER_MESSAGE(Symbol('CATS_SERVICE'), 'AppModule'),
|
||||
);
|
||||
|
||||
expect(actualMessage).toBe(expectedMessage('Symbol(CATS_SERVICE)'));
|
||||
});
|
||||
|
||||
it('should display a class token', () => {
|
||||
class CatsService {}
|
||||
|
||||
const actualMessage = stringCleaner(
|
||||
INVALID_PROVIDER_MESSAGE(CatsService, 'AppModule'),
|
||||
);
|
||||
|
||||
expect(actualMessage).toBe(expectedMessage('CatsService'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('INVALID_MODULE_MESSAGE', () => {
|
||||
it('should display the received `null` value and its type', () => {
|
||||
const expectedMessage =
|
||||
|
||||
@@ -33,6 +33,30 @@ describe('NestContainer', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each([undefined, null])(
|
||||
'should "addController" throw "CircularDependencyException" naming the module when controller is %s',
|
||||
async nilValue => {
|
||||
const { moduleRef } = (await container.addModule(TestModule, []))!;
|
||||
const addController = () =>
|
||||
container.addController(nilValue!, moduleRef.token);
|
||||
|
||||
expect(addController).toThrow(CircularDependencyException);
|
||||
expect(addController).toThrow('inside TestModule');
|
||||
},
|
||||
);
|
||||
|
||||
it.each([undefined, null])(
|
||||
'should "addExportedProviderOrModule" throw "CircularDependencyException" naming the module when exported item is %s',
|
||||
async nilValue => {
|
||||
const { moduleRef } = (await container.addModule(TestModule, []))!;
|
||||
const addExport = () =>
|
||||
container.addExportedProviderOrModule(nilValue!, moduleRef.token);
|
||||
|
||||
expect(addExport).toThrow(CircularDependencyException);
|
||||
expect(addExport).toThrow('inside TestModule');
|
||||
},
|
||||
);
|
||||
|
||||
it('should "addController" throw "UnknownModuleException" when module is not stored in collection', () => {
|
||||
expect(() => container.addController(null!, 'TestModule')).toThrow(
|
||||
UnknownModuleException,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Controller, Scope } from '@nestjs/common';
|
||||
import { Module as ModuleDecorator } from '../../../common/decorators/modules/module.decorator.js';
|
||||
import { Injectable } from '../../../common/index.js';
|
||||
import { InvalidProviderException } from '../../errors/exceptions/invalid-provider.exception.js';
|
||||
import { RuntimeException } from '../../errors/exceptions/runtime.exception.js';
|
||||
import { UnknownElementException } from '../../errors/exceptions/unknown-element.exception.js';
|
||||
import { UnknownExportException } from '../../errors/exceptions/unknown-export.exception.js';
|
||||
@@ -74,9 +75,21 @@ describe('Module', () => {
|
||||
it('should call `addCustomProvider`', () => {
|
||||
const addCustomProviderSpy = vi.spyOn(moduleRef, 'addCustomProvider');
|
||||
|
||||
moduleRef.addInjectable({ provide: 'test' } as any, 'guard');
|
||||
moduleRef.addInjectable(
|
||||
{ provide: 'test', useValue: 'test' } as any,
|
||||
'guard',
|
||||
);
|
||||
expect(addCustomProviderSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw when a custom injectable does not define a valid "use*" property', () => {
|
||||
expect(() =>
|
||||
moduleRef.addInjectable(
|
||||
{ provide: 'test', useClass: undefined } as any,
|
||||
'guard',
|
||||
),
|
||||
).toThrow(InvalidProviderException);
|
||||
});
|
||||
});
|
||||
|
||||
it('should add provider', () => {
|
||||
@@ -158,6 +171,38 @@ describe('Module', () => {
|
||||
expect(addCustomUseExisting).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('when custom provider does not define a valid "useClass", "useValue", "useFactory" or "useExisting"', () => {
|
||||
const expectInvalidProvider = (provider: object) => {
|
||||
const addCustomProvider = () =>
|
||||
moduleRef.addCustomProvider(provider as any, new Map());
|
||||
|
||||
expect(addCustomProvider).toThrow(InvalidProviderException);
|
||||
expect(addCustomProvider).toThrow(
|
||||
'The provider "test" in the TestModule "providers" array',
|
||||
);
|
||||
};
|
||||
|
||||
it('should throw when "useClass" is undefined', () => {
|
||||
expectInvalidProvider({ provide: 'test', useClass: undefined });
|
||||
});
|
||||
|
||||
it('should throw when "useClass" is null', () => {
|
||||
expectInvalidProvider({ provide: 'test', useClass: null });
|
||||
});
|
||||
|
||||
it('should throw when "useExisting" is undefined', () => {
|
||||
expectInvalidProvider({ provide: 'test', useExisting: undefined });
|
||||
});
|
||||
|
||||
it('should throw when "useFactory" is null', () => {
|
||||
expectInvalidProvider({ provide: 'test', useFactory: null });
|
||||
});
|
||||
|
||||
it('should throw when none of them is set', () => {
|
||||
expectInvalidProvider({ provide: 'test' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('addCustomClass', () => {
|
||||
const type = { name: 'TypeTest' };
|
||||
const provider = { provide: type, useClass: type, durable: true };
|
||||
|
||||
Reference in new issue
Block a user