diff --git a/gulpfile.ts b/gulpfile.ts index 2bd0f3e078..0b792c31dc 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -204,7 +204,7 @@ export class Gulpfile { */ @SequenceTask() tests() { - return ["compile", "tslint", "coveragePost", "coverageRemap"]; + return ["tslint", "compile", "coveragePost", "coverageRemap"]; } } diff --git a/src/container.ts b/src/container.ts index cc18963daf..1ae7241b7c 100644 --- a/src/container.ts +++ b/src/container.ts @@ -1,4 +1,12 @@ +type DefaultContainer = { + get(someClass: { new(...args: any[]): T } | Function): T; + getInstances(): Entry[]; + isEmpty(): boolean; +}; + +export type Entry = { type: Function, object: any }; + /** * Container options. */ @@ -14,15 +22,20 @@ export interface UseContainerOptions { */ fallbackOnErrors?: boolean; + /** + * If the default container has registered instances this function allow to class-validator to expose the current state. + * @param instances + */ + registerExistingInstances?: (instances: Entry[]) => void; } /** * Container to be used by this library for inversion control. If container was not implicitly set then by default * container simply creates a new instance of the given class. */ -const defaultContainer: { get(someClass: { new (...args: any[]): T }|Function): T } = new (class { - private instances: { type: Function, object: any }[] = []; - get(someClass: { new (...args: any[]): T }): T { +const defaultContainer: DefaultContainer = new (class { + private instances: Entry[] = []; + get(someClass: { new(...args: any[]): T }): T { let instance = this.instances.find(instance => instance.type === someClass); if (!instance) { instance = { type: someClass, object: new someClass() }; @@ -31,9 +44,17 @@ const defaultContainer: { get(someClass: { new (...args: any[]): T }|Function return instance.object; } + + getInstances(): Entry[] { + return this.instances; + } + + isEmpty(): boolean { + return this.instances.length === 0; + } })(); -let userContainer: { get(someClass: { new (...args: any[]): T }|Function): T }; +let userContainer: { get(someClass: { new(...args: any[]): T } | Function): T }; let userContainerOptions: UseContainerOptions; /** @@ -42,12 +63,20 @@ let userContainerOptions: UseContainerOptions; export function useContainer(iocContainer: { get(someClass: any): any }, options?: UseContainerOptions) { userContainer = iocContainer; userContainerOptions = options; + + if (userContainer && !defaultContainer.isEmpty()) { + if (options && typeof (options.registerExistingInstances) === "function") { + options.registerExistingInstances(defaultContainer.getInstances()); + } else { + throw new Error("class-validator is unable to register existing components on userContainer. Please provide a 'registerExistingInstances' function in options."); + } + } } /** * Gets the IOC container used by this library. */ -export function getFromContainer(someClass: { new (...args: any[]): T }|Function): T { +export function getFromContainer(someClass: { new(...args: any[]): T } | Function): T { if (userContainer) { try { const instance = userContainer.get(someClass); diff --git a/test/functional/container.spec.ts b/test/functional/container.spec.ts new file mode 100644 index 0000000000..2e69e6e27d --- /dev/null +++ b/test/functional/container.spec.ts @@ -0,0 +1,74 @@ +import "es6-shim"; +import { IsNotEmpty } from "../../src/decorator/decorators"; +import { Validator } from "../../src/validation/Validator"; +import { should, use } from "chai"; + +import * as chaiAsPromised from "chai-as-promised"; +import { useContainer } from "../../src"; + +should(); +use(chaiAsPromised); + +// ------------------------------------------------------------------------- +// Setup +// ------------------------------------------------------------------------- + +const validator = new Validator(); + +// ------------------------------------------------------------------------- +// Specifications: common decorators +// ------------------------------------------------------------------------- + +describe("container", function () { + afterEach(function () { + useContainer(undefined); + }); + + it("should throw an error if defaultContainer is not empty and useContainer hasn't registerExistingInstances function in options", function () { + class MyClass { + @IsNotEmpty() + title: string; + } + + const model = new MyClass(); + + try { + useContainer({ get: () => { null; } }); + } catch (err) { + err.message.should.be.equal("class-validator is unable to register existing components on userContainer. Please provide a \'registerExistingInstances\' function in options."); + } + }); + + it("should invoke registerExistingInstances function with instances", function () { + const instances: { type: Function, object: any }[] = []; + + const container = { + get(someClass: { new(...args: any[]): T }): T { + let instance = instances.find(instance => instance.type === someClass); + if (!instance) { + instance = { type: someClass, object: new someClass() }; + instances.push(instance); + } + + return instance.object; + } + }; + + const options = { + registerExistingInstances: (newInstances: any[]) => { + newInstances.forEach(instance => instances.push(instance)); + } + }; + + useContainer(container, options); + + class MyClass { + @IsNotEmpty() + title: string; + } + + const model = new MyClass(); + + instances.length.should.not.be.equal(0); + }); +}); \ No newline at end of file