diff --git a/docs/README.md b/docs/README.md index 4125c41d88..f232637578 100644 --- a/docs/README.md +++ b/docs/README.md @@ -77,7 +77,6 @@ export const AppModule: FoalModule = { controllers: [ rest.attachService('/horses', HorseService) ], - services: [ HorseService ] }; ``` diff --git a/docs/basics/controllers.md b/docs/basics/controllers.md index 68d4a0da4f..5bd40f0483 100644 --- a/docs/basics/controllers.md +++ b/docs/basics/controllers.md @@ -34,7 +34,6 @@ import { rest } from '@foal/common'; import { FoalModule } from '@foal/core'; const AppModule: FoalModule = { - services: [ User ], controllers: [ rest.attachService('/users', User) ] } ``` @@ -60,7 +59,6 @@ class User implements PartialCRUDService { } const foal = new Foal({ - services: [ User ], controllers: [ rest.attachService('/users', User) ] }); diff --git a/docs/basics/modules.md b/docs/basics/modules.md index dbeabd9452..48188721ff 100644 --- a/docs/basics/modules.md +++ b/docs/basics/modules.md @@ -1,27 +1,81 @@ # Modules -Every app starts with a module. A module instantiates services and binds controllers to the request handler. It may import other ones. They're espacially interesting when a project grows up. +Every app starts with a module. A module instantiates services and binds controllers to the request handler. It may also have pre-hooks (or post-hooks) executed before (or after) every controller. ## Example ```typescript import { rest } from '@foal/common'; import { FoalModule } from '@foal/core'; -// module and service imports ... +// module and service imports... const AppModule: FoaModule = { - services: [ MyService1, MyService2, MyCRUDService ], controllers: [ rest.attachService('/my_resources', MyCRUDService) ], - imports: [ - { module: MyModule } - { module: Team1Module, path: '/team1' }, - { module: Team2Module, path: '/team2' }, + hooks: [ + myFirstPreHook(), + mySecondPreHook(), + myPostHook() ] } ``` -## Dependencies +## Nested modules -![Schema](./module-dependencies.png) \ No newline at end of file +When your app grows up, you may be interested in splitting your app into several modules. Here's an example on how to embed your modules: + +```typescript +const Module1: FoalModule = { + controllers: [ + rest.attachService('/my_resources', MyCRUDService) + ] +}; + +const Module2: FoalModule = { + controllers: [ + rest.attachService('/my_resources2', MyCRUDService2) + ] +}; + +const AppModule: FoalModule = { + controllers: [ + rest.attachService('/my_resources3', MyCRUDService3) + ], + modules: [ + { module: Module1 } + { module: Module2, path: '/foo' }, + ] +} + +/** + * The app serves three REST endpoints: + * - /my_resources3 + * - /my_resources + * - /foo/my_resources2 + */ +``` + +Each service is instanciated per module. If you want to share the same instance of a service accross multiple modules, you must specify the service class in the `services` property of a parent module. + +```typescript +const Module1: FoalModule = { + controllers: [ + rest.attachService('/my_resources', MySharedCRUDService) + ] +}; + +const Module2: FoalModule = { + controllers: [ + rest.attachService('/my_resources2', MySharedCRUDService) + ] +}; + +const AppModule: FoalModule = { + modules: [ + { module: Module1 } + { module: Module2, path: '/foo' }, + ], + services: [ MySharedCRUDService ] +} +``` \ No newline at end of file diff --git a/docs/packages/ejs.md b/docs/packages/ejs.md index 5166fe7bbc..0bbccdfebd 100644 --- a/docs/packages/ejs.md +++ b/docs/packages/ejs.md @@ -45,7 +45,6 @@ export const AppModule: FoalModule = { controllers: [ view.attachService('/', IndexViewService), ], - services: [ IndexViewService ], }; ``` \ No newline at end of file diff --git a/docs/packages/sequelize.md b/docs/packages/sequelize.md index 39f674845d..70d6f1f39f 100644 --- a/docs/packages/sequelize.md +++ b/docs/packages/sequelize.md @@ -57,7 +57,6 @@ import { Connection } from './connection.service'; import { User } from './user.service'; const foal = new Foal({ - services: [ Connection, User ], controllers: [ rest.attachService('/users', User) ] }); diff --git a/packages/core/src/factories/controller-factory.spec.ts b/packages/core/src/factories/controller-factory.spec.ts index 0b63b14c72..95f1a6399e 100644 --- a/packages/core/src/factories/controller-factory.spec.ts +++ b/packages/core/src/factories/controller-factory.spec.ts @@ -72,20 +72,8 @@ describe('ControllerFactory', () => { describe('when attachService(path: string, ServiceClass: Type) is called', () => { - describe('with a ServiceClass that has not been added to the service manager', () => { - - it('should raise an Error.', () => { - const func = controllerFactory.attachService('/my_path', ServiceClass); - - expect(() => func(services)).to.throw(); - }); - - }); - describe('with good parameters', () => { - beforeEach(() => services.add(ServiceClass)); - it('should return a LowLevelRoute array from the Route array of the getRoutes method.', async () => { const func = controllerFactory.attachService('/my_path', ServiceClass); const lowLevelRoutes = func(services); diff --git a/packages/core/src/factories/controller-factory.ts b/packages/core/src/factories/controller-factory.ts index c8f1555fc8..7ed0f4cc79 100644 --- a/packages/core/src/factories/controller-factory.ts +++ b/packages/core/src/factories/controller-factory.ts @@ -18,10 +18,6 @@ export abstract class ControllerFactory { return (services: ServiceManager): LowLevelRoute[] => { const service = services.get(ServiceClass); - if (!service) { - throw new Error(`${ServiceClass.name} should be declared in a module.`); - } - return this.getRoutes(service).map(route => { const middlewares = [ ...this.getPreMiddlewares(ServiceClass, route.serviceMethodName), diff --git a/packages/core/src/foal.spec.ts b/packages/core/src/foal.spec.ts index fbf34cb894..65ab013f1a 100644 --- a/packages/core/src/foal.spec.ts +++ b/packages/core/src/foal.spec.ts @@ -13,25 +13,33 @@ describe('Foal', () => { constructor() {} } const foalModule1: FoalModule = { - services: [ Foobar ] + services: [] }; const foalModule2: FoalModule = { - services: [] + services: [ Foobar ] }; - it('should create an serviceManager.', () => { + it('should create a serviceManager.', () => { const foal1 = new Foal(foalModule1); expect(foal1.services).to.not.be.an('undefined'); - expect(foal1.services.get(Foobar)).to.be.instanceof(Foobar); }); - it('should create an serviceManager from the parentModule if it exists.', () => { + it('should create a serviceManager from the parentModule if it exists.', () => { + const foal1 = new Foal(foalModule1); + const foal2 = new Foal(foalModule2, foal1); + + expect(foal2.services).to.not.equal(foal1.services); + expect(foal2.services.parentServiceManager).to.equal(foal1.services); + }); + + it('should instantiate the services given in the services array.', () => { + // When calling `services.get` the services are instantiated if + // they do not already exist. The only way to test if there are created + // before is using the prototype schema of the ServiceManager. const foal1 = new Foal(foalModule1); const foal2 = new Foal(foalModule2, foal1); - expect(foal1.services).to.not.equal(foal2.services); - expect(foal1.services.get(Foobar)).to.not.be.an('undefined'); - expect(foal2.services.get(Foobar)).to.equal(foal1.services.get(Foobar)); + expect(foal2.services.get(Foobar)).not.to.equal(foal1.services.get(Foobar)); }); xit('should create lowLevelRoutes from the controller routes.', () => { diff --git a/packages/core/src/foal.ts b/packages/core/src/foal.ts index cf7bca011e..d134eabde7 100644 --- a/packages/core/src/foal.ts +++ b/packages/core/src/foal.ts @@ -17,6 +17,7 @@ export class Foal { const controllers = foalModule.controllers || []; const modules = foalModule.modules || []; const moduleHooks = foalModule.hooks || []; + const services = foalModule.services || []; if (parentModule) { this.services = new ServiceManager(parentModule.services); @@ -24,7 +25,8 @@ export class Foal { this.services = new ServiceManager(); } - foalModule.services.forEach(service => this.services.add(service)); + // Instantiate the services. + services.forEach(service => this.services.get(service)); const { modulePreMiddlewares, modulePostMiddlewares } = this.getMiddlewares(moduleHooks); diff --git a/packages/core/src/interfaces/module.ts b/packages/core/src/interfaces/module.ts index 2cd69b6bf7..fb9ed972cd 100644 --- a/packages/core/src/interfaces/module.ts +++ b/packages/core/src/interfaces/module.ts @@ -2,7 +2,7 @@ import { Controller } from './controller-and-routes'; import { Hook, Type } from './utils'; export interface FoalModule { - services: Type[]; + services?: Type[]; controllers?: Controller[]; hooks?: Hook[]; modules?: { module: FoalModule, path?: string }[]; diff --git a/packages/core/src/service-manager.spec.ts b/packages/core/src/service-manager.spec.ts index 06c61da4ab..9860151b7d 100644 --- a/packages/core/src/service-manager.spec.ts +++ b/packages/core/src/service-manager.spec.ts @@ -4,97 +4,82 @@ import { Service, ServiceManager } from './service-manager'; describe('ServiceManager', () => { - describe('instantiated with no parent serviceManager', () => { - let serviceManager: ServiceManager; + let serviceManager: ServiceManager; - @Service() - class Foobar { - constructor() {} - } + @Service() + class Foobar { + constructor() {} + } - beforeEach(() => serviceManager = new ServiceManager()); + beforeEach(() => serviceManager = new ServiceManager()); - describe('when add(Service: Type): void is called', () => { + describe('when get(Service: Type): T is called', () => { - it('should raise an exception if the given Service is not a service class.', () => { - class Foo {} + it('should throw an exception if the given Service is not a service class.', () => { + class Foo {} - @Service() - class Bar {} + @Service() + class Bar {} - class Barfoo { - constructor() {} - } - - expect(() => serviceManager.add(Foo)).to.throw(Error); - expect(() => serviceManager.add(Bar)).to.throw(); - expect(() => serviceManager.add(Barfoo)).to.throw(); - }); - - it('should instantiate the given Service.', () => { - serviceManager.add(Foobar); - - expect(serviceManager.get(Foobar)).to.be.an.instanceof(Foobar); - }); - - it('should instantiate the dependencies of the given Service.', () => { - @Service() - class Foobar2 { - constructor(public foobar: Foobar) {} - } - - serviceManager.add(Foobar2); - const foobar = serviceManager.get(Foobar); - - expect(foobar).to.be.an.instanceof(Foobar); - expect(serviceManager.get(Foobar2).foobar).to.equal(foobar); - }); - - it('should not instantiate twice the given Service.', () => { - serviceManager.add(Foobar); - const foobar = serviceManager.get(Foobar); - serviceManager.add(Foobar); - - expect(serviceManager.get(Foobar)).to.equal(foobar); - }); + class Barfoo { + constructor() {} + } + expect(() => serviceManager.get(Foo)).to.throw(); + expect(() => serviceManager.get(Bar)).to.throw(); + expect(() => serviceManager.get(Barfoo)).to.throw(); }); - }); - - describe('instantiated with a parent serviceManager', () => { - let parentServiceManager: ServiceManager; - let serviceManager: ServiceManager; + it('should return an instance of the given Service.', () => { + expect(serviceManager.get(Foobar)).to.be.an.instanceof(Foobar); + }); - @Service() - class Foobar { - constructor() {} - } + it('should always return the same value for the same given Service.', () => { + expect(serviceManager.get(Foobar)).to.equal(serviceManager.get(Foobar)); + }); - beforeEach(() => { - parentServiceManager = new ServiceManager(); + it('should follow the prototype pattern with its parentServiceManager.', () => { + const parentServiceManager = new ServiceManager(); serviceManager = new ServiceManager(parentServiceManager); - }); - describe('when get(Service: Type): T is called', () => { + // Instantiate the service. + const parentFoobar = parentServiceManager.get(Foobar); + const childFoobar = serviceManager.get(Foobar); - it('should return the Service instance of the parent if it exists.', () => { - parentServiceManager.add(Foobar); - const foobar = parentServiceManager.get(Foobar); - expect(serviceManager.get(Foobar)).to.equal(foobar); - }); + expect(childFoobar).to.equal(parentFoobar); - }); + @Service() + class Foobar2 { + constructor() {} + } - describe('when add(Service: Type): void is called', () => { + const childFoobar2 = serviceManager.get(Foobar2); + const parentFoobar2 = parentServiceManager.get(Foobar2); + const childFoobar2b = serviceManager.get(Foobar2); - it('should not instantiate the Service if it is instantiated in the parent serviceManager.', () => { - parentServiceManager.add(Foobar); - const foobar = parentServiceManager.get(Foobar); - serviceManager.add(Foobar); - expect(serviceManager.get(Foobar)).to.equal(foobar); - }); + expect(childFoobar2).to.not.equal(parentFoobar2); + expect(childFoobar2b).to.equal(childFoobar2); + }); + it('should return an instance of the given Service which dependencies are instances that can be retreived' + + ' by the same method.', () => { + @Service() + class Foobar2 { + constructor() {} + } + + @Service() + class Foobar3 { + constructor(public foobar: Foobar, public foobar2: Foobar2) {} + } + + // foobar3 is "gotten" in the middle on purpose. + const foobar = serviceManager.get(Foobar); + const foobar3 = serviceManager.get(Foobar3); + const foobar2 = serviceManager.get(Foobar2); + + expect(foobar3.foobar).to.equal(foobar); + expect(foobar3.foobar2).to.equal(foobar2); }); }); diff --git a/packages/core/src/service-manager.ts b/packages/core/src/service-manager.ts index 4d269bb922..54214fdd9f 100644 --- a/packages/core/src/service-manager.ts +++ b/packages/core/src/service-manager.ts @@ -8,31 +8,32 @@ export function Service() { export class ServiceManager { - private map: Map, any> = new Map(); + public map: Map, any> = new Map(); - constructor(private parentServiceManager?: ServiceManager) {} + constructor(readonly parentServiceManager?: ServiceManager) {} - public add(Service: Type): void { - if (this.map.get(Service) || (this.parentServiceManager && this.parentServiceManager.get(Service))) { - return; + public get(Service: Type): T { + // Get the service using a prototype pattern. + if (this.map.get(Service)) { + return this.map.get(Service); + } + if (this.parentServiceManager && this.parentServiceManager.map.get(Service)) { + return this.parentServiceManager.map.get(Service); } - const dependencies: Type[] = Reflect.getMetadata('design:paramtypes', Service); - if (!dependencies) { + + // If the service has not been instantiated yet, then instantiate it in this service manager. + const dependencies = Reflect.getMetadata('design:paramtypes', Service); + if (!Array.isArray(dependencies)) { throw new Error(`${Service.name} has no dependencies. Please check that: - The service has a constructor. - The service has the @Service() decorator. - The "emitDecoratorMetadata" is set to true in the tsconfig.json file.`); } - if (dependencies.length > 0) { - dependencies.forEach(dep => this.add(dep)); - } - this.map.set(Service, new Service( - ...dependencies.map(Dep => this.map.get(Dep) || (this.parentServiceManager && this.parentServiceManager.get(Dep))) - )); - } + const service = new Service(...dependencies.map(Dep => this.get(Dep))); - public get(Service: Type): T { - return this.map.get(Service) || (this.parentServiceManager && this.parentServiceManager.get(Service)) as T; + // Save and return the service. + this.map.set(Service, service); + return service; } } diff --git a/packages/core/src/utils/metadatas.spec.ts b/packages/core/src/utils/metadatas.spec.ts new file mode 100644 index 0000000000..ad54519306 --- /dev/null +++ b/packages/core/src/utils/metadatas.spec.ts @@ -0,0 +1,45 @@ +import { expect } from 'chai'; +import 'reflect-metadata'; + +import { defineMetadata, getMetadata } from './metadatas'; + +describe('defineMetadata(metadataKey: any, metadataValue: any, target: any, ' + + 'propertyKey: string|undefined): any', () => { + + it('should add the given metadata to the class if the propertyKey is undefined.', () => { + class Foobar {} + defineMetadata('foo', 'my metadata', Foobar, undefined); + + expect(Reflect.getMetadata('foo', Foobar)).to.equal('my metadata'); + }); + + it('should add the given metadata to the method if the propertyKey is defined.', () => { + class Foobar { + public bar() {} + } + defineMetadata('foo', 'my metadata', Foobar, 'bar'); + + expect(Reflect.getMetadata('foo', Foobar, 'bar')).to.equal('my metadata'); + }); + +}); + +describe('getMetadata(metadataKey: any, target: any, propertyKey: string|undefined): any', () => { + + it('should get the given metadata from the class if the propertyKey is undefined.', () => { + class Foobar {} + Reflect.defineMetadata('foo', 'my metadata', Foobar); + + expect(getMetadata('foo', Foobar, undefined)).to.equal('my metadata'); + }); + + it('should get the given metadata from the method if the propertyKey is defined.', () => { + class Foobar { + public bar() {} + } + Reflect.defineMetadata('foo', 'my metadata', Foobar, 'bar'); + + expect(getMetadata('foo', Foobar, 'bar')).to.equal('my metadata'); + }); + +}); diff --git a/packages/examples/src/app/app.module.ts b/packages/examples/src/app/app.module.ts index 67b26a7d0b..66ed1efcb1 100644 --- a/packages/examples/src/app/app.module.ts +++ b/packages/examples/src/app/app.module.ts @@ -1,7 +1,7 @@ import { afterThatLog, log, rest, view } from '@foal/common'; import { FoalModule } from '@foal/core'; -import { ConnectionService, IndexViewService, UserService } from './services'; +import { IndexViewService, UserService } from './services'; export const AppModule: FoalModule = { controllers: [ @@ -13,6 +13,5 @@ export const AppModule: FoalModule = { log('AppModule2'), afterThatLog('AppModule1 (post)'), afterThatLog('AppModule2 (post)'), - ], - services: [ ConnectionService, UserService, IndexViewService ], + ] };