Skip to content

Commit

Permalink
Merge pull request #48 from FoalTS/optional-service-declaration
Browse files Browse the repository at this point in the history
Make service declaration optional
  • Loading branch information
LoicPoullain authored Dec 28, 2017
2 parents 3f8feeb + 80fbf2d commit 38f705b
Show file tree
Hide file tree
Showing 14 changed files with 205 additions and 132 deletions.
1 change: 0 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ export const AppModule: FoalModule = {
controllers: [
rest.attachService('/horses', HorseService)
],
services: [ HorseService ]
};
```

Expand Down
2 changes: 0 additions & 2 deletions docs/basics/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import { rest } from '@foal/common';
import { FoalModule } from '@foal/core';

const AppModule: FoalModule = {
services: [ User ],
controllers: [ rest.attachService('/users', User) ]
}
```
Expand All @@ -60,7 +59,6 @@ class User implements PartialCRUDService {
}

const foal = new Foal({
services: [ User ],
controllers: [ rest.attachService('/users', User) ]
});

Expand Down
72 changes: 63 additions & 9 deletions docs/basics/modules.md
Original file line number Diff line number Diff line change
@@ -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)
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 ]
}
```
1 change: 0 additions & 1 deletion docs/packages/ejs.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export const AppModule: FoalModule = {
controllers: [
view.attachService('/', IndexViewService),
],
services: [ IndexViewService ],
};

```
1 change: 0 additions & 1 deletion docs/packages/sequelize.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) ]
});

Expand Down
12 changes: 0 additions & 12 deletions packages/core/src/factories/controller-factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,8 @@ describe('ControllerFactory<T>', () => {

describe('when attachService(path: string, ServiceClass: Type<T>) 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);
Expand Down
4 changes: 0 additions & 4 deletions packages/core/src/factories/controller-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ export abstract class ControllerFactory<T> {
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),
Expand Down
24 changes: 16 additions & 8 deletions packages/core/src/foal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.', () => {
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/foal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@ 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);
} else {
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);

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/interfaces/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Controller } from './controller-and-routes';
import { Hook, Type } from './utils';

export interface FoalModule {
services: Type<any>[];
services?: Type<any>[];
controllers?: Controller[];
hooks?: Hook[];
modules?: { module: FoalModule, path?: string }[];
Expand Down
131 changes: 58 additions & 73 deletions packages/core/src/service-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>): void is called', () => {
describe('when get<T>(Service: Type<T>): 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<T>(Service: Type<T>): 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<any>): 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);
});

});
Expand Down
Loading

0 comments on commit 38f705b

Please sign in to comment.