Skip to content

Commit da79e1b

Browse files
committed
Merge branch 'crontroller-inheritance' of https://github.com/ivanproskuryakov/routing-controllers into ivanproskuryakov-crontroller-inheritance
2 parents 70a3465 + 3618cf8 commit da79e1b

File tree

14 files changed

+350
-82
lines changed

14 files changed

+350
-82
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ You can use routing-controllers with [express.js][1] or [koa.js][2].
5555
+ [Interceptor classes](#interceptor-classes)
5656
+ [Global interceptors](#global-interceptors)
5757
* [Creating instances of classes from action params](#creating-instances-of-classes-from-action-params)
58+
* [Controller inheritance](#controller-inheritance)
5859
* [Auto validating action params](#auto-validating-action-params)
5960
* [Using authorization features](#using-authorization-features)
6061
- [@Authorized decorator](#authorized-decorator)
@@ -1236,6 +1237,33 @@ Learn more about class-transformer and how to handle more complex object constru
12361237
This behaviour is enabled by default.
12371238
If you want to disable it simply pass `classTransformer: false` to createExpressServer method. Alternatively you can disable transforming for [individual controllers or routes](#selectively-disable-requestresponse-transforming).
12381239

1240+
## Controller Inheritance
1241+
Often your application may need to have an option to inherit controller from another to reuse code and void duplication.
1242+
A good example of the use is the CRUD operations which can be hidden inside `AbstractBaseController` with the possibility to add new and overload methods, the template method pattern.
1243+
1244+
```typescript
1245+
@Controller(`/product`)
1246+
class ProductController extends AbstractControllerTemplate {}
1247+
@Controller(`/category`)
1248+
class CategoryController extends AbstractControllerTemplate {}
1249+
abstract class AbstractControllerTemplate {
1250+
@Post()
1251+
public create() {}
1252+
1253+
@Read()
1254+
public read() {}
1255+
1256+
@Put()
1257+
public update() {}
1258+
1259+
@Delete()
1260+
public delete() {}
1261+
}
1262+
1263+
```
1264+
https://en.wikipedia.org/wiki/Template_method_pattern
1265+
1266+
12391267
## Auto validating action params
12401268

12411269
Sometimes parsing a json object into instance of some class is not enough.

sample/sample17-controllers-inheritance/app.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,12 @@ useExpressServer(app, {
88
});
99
app.listen(3001); // run express app
1010

11-
console.log("Express server is running on port 3001. Open http://localhost:3001/blogs/ or http://localhost:3002/posts/");
11+
console.log(
12+
"Possible GET endpoints you may see from a browser",
13+
"http://localhost:3001/article",
14+
"http://localhost:3001/article/1000",
15+
"http://localhost:3001/product",
16+
"http://localhost:3001/product/1000",
17+
"http://localhost:3001/category",
18+
"http://localhost:3001/category/1000",
19+
);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {Res} from "../../../src/decorator/Res";
2+
import {Put} from "../../../src/decorator/Put";
3+
import {Post} from "../../../src/decorator/Post";
4+
import {Param} from "../../../src/decorator/Param";
5+
import {Get} from "../../../src/decorator/Get";
6+
import {Delete} from "../../../src/decorator/Delete";
7+
import {Body} from "../../../src/decorator/Body";
8+
9+
import {MockedRepository} from "../repository/MockedRepository";
10+
import {IInstance} from "../interface/IInstance";
11+
12+
/**
13+
* @description the base controller class used by derivatives
14+
*/
15+
export abstract class AbstractControllerTemplate {
16+
/**
17+
* @description domain part of a system, also called object|entity|model
18+
*/
19+
protected domain: string;
20+
protected repository: MockedRepository;
21+
22+
@Post()
23+
public async create(
24+
@Body() payload: any,
25+
@Res() res: any
26+
): Promise<{}> {
27+
const item = await this.repository.create(payload);
28+
29+
res.status(201);
30+
res.location(`/${this.domain}/${item.id}`);
31+
32+
return {};
33+
}
34+
35+
@Put("/:id")
36+
public async updated(
37+
@Param("id") id: number,
38+
@Body() payload: any,
39+
@Res() res: any
40+
): Promise<{}> {
41+
await this.repository.update(id, payload);
42+
res.status(204);
43+
44+
return {};
45+
}
46+
47+
@Get("/:id")
48+
public read(
49+
@Param("id") id: number,
50+
@Res() res: any
51+
): Promise<IInstance> {
52+
return this.repository.find(id);
53+
}
54+
55+
@Get()
56+
public readCollection(
57+
@Res() res: any
58+
): Promise<IInstance[]> {
59+
return this.repository.getCollection();
60+
}
61+
62+
@Delete("/:id")
63+
public async delete(
64+
@Param("id") id: number,
65+
@Res() res: any
66+
): Promise<{}> {
67+
await this.repository.delete(id);
68+
69+
return {};
70+
}
71+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {Controller} from "../../../src/decorator/Controller";
2+
import {AbstractControllerTemplate} from "./AbstractContollerTemplate";
3+
import {MockedRepository} from "../repository/MockedRepository";
4+
5+
const domain = "article";
6+
7+
@Controller(`/${domain}`)
8+
export class ArticleController extends AbstractControllerTemplate {
9+
protected constructor() {
10+
super();
11+
12+
this.domain = domain;
13+
this.repository = new MockedRepository(domain);
14+
}
15+
}

sample/sample17-controllers-inheritance/controllers/BaseController.ts

Lines changed: 0 additions & 56 deletions
This file was deleted.

sample/sample17-controllers-inheritance/controllers/BlogController.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {Controller} from "../../../src/decorator/Controller";
2+
import {AbstractControllerTemplate} from "./AbstractContollerTemplate";
3+
import {MockedRepository} from "../repository/MockedRepository";
4+
5+
const domain = "category";
6+
7+
@Controller(`/${domain}`)
8+
export class CategoryController extends AbstractControllerTemplate {
9+
protected constructor() {
10+
super();
11+
12+
this.domain = domain;
13+
this.repository = new MockedRepository(domain);
14+
}
15+
}

sample/sample17-controllers-inheritance/controllers/PostController.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {Controller} from "../../../src/decorator/Controller";
2+
import {AbstractControllerTemplate} from "./AbstractContollerTemplate";
3+
import {MockedRepository} from "../repository/MockedRepository";
4+
5+
const domain = "product";
6+
7+
@Controller(`/${domain}`)
8+
export class ProductController extends AbstractControllerTemplate {
9+
protected constructor() {
10+
super();
11+
12+
this.domain = domain;
13+
this.repository = new MockedRepository(domain);
14+
}
15+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface IInstance {
2+
id: number;
3+
type: string;
4+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface IPayload {
2+
id: number;
3+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {IInstance} from "../interface/IInstance";
2+
import {IPayload} from "../interface/IPayload";
3+
4+
export class MockedRepository {
5+
protected domain: string;
6+
7+
constructor(domain: string) {
8+
this.domain = domain;
9+
}
10+
11+
/**
12+
* @description Dummy method to return collection of items
13+
*/
14+
public getCollection(): Promise<IInstance[]> {
15+
return Promise.resolve([
16+
{
17+
id: 10020,
18+
type: this.domain
19+
},
20+
{
21+
id: 10001,
22+
type: this.domain
23+
},
24+
{
25+
id: 10002,
26+
type: this.domain
27+
},
28+
]);
29+
}
30+
31+
/**
32+
* @description Dummy method to create a new item in storage and return its instance
33+
*/
34+
public create(payload: IPayload): Promise<IInstance> {
35+
return Promise.resolve(
36+
{
37+
id: 10000,
38+
type: this.domain
39+
}
40+
);
41+
}
42+
43+
/**
44+
* @description Dummy method to find item in storage
45+
*/
46+
public find(id: number): Promise<IInstance> {
47+
return Promise.resolve(
48+
{
49+
id: id,
50+
type: this.domain
51+
}
52+
);
53+
}
54+
55+
/**
56+
* @description Dummy method to delete item in storage by id
57+
*/
58+
public delete(id: number): Promise<void> {
59+
return Promise.resolve();
60+
}
61+
62+
/**
63+
* @description Dummy method to update item in storage by id
64+
*/
65+
public update(id: number, payload: IPayload): Promise<IInstance> {
66+
return Promise.resolve(
67+
{
68+
id: 10000,
69+
type: this.domain
70+
}
71+
);
72+
}
73+
74+
}
75+

src/metadata-builder/MetadataBuilder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class MetadataBuilder {
2424
/**
2525
* Builds controller metadata from a registered controller metadata args.
2626
*/
27-
buildControllerMetadata(classes?: Function[]) {
27+
buildControllerMetadata(classes?: Function[]): ControllerMetadata[] {
2828
return this.createControllers(classes);
2929
}
3030

0 commit comments

Comments
 (0)