-
Notifications
You must be signed in to change notification settings - Fork 402
feat: controller inheritance #301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no tests cases for this new feature so we don't know if it really works. Secondly, you need to write some words about it in readme, so other users may use it too. And I don't know if we really need this feature as we discussed in #147.
What's up with this PR @19majkel94 and @ivanpadavan? Do we need this, is it well covered with tests, etc? |
Anything I can do, to move this pull request forward? |
First, some word in the README and tests. When it has it we can see if it's works as intended or not. But first it would be really nice if you could reason why you need this change. As in the linked issue is mentioned, neither of us (maintainers) convienced it's a thing we need. As @19majkel94 wrote: composition over inheritance is the way. I just cannot see a real usecase for this when controller inheritance can save you a lot of time. Except if you write a very basic CRUD app without authorization logic or validation. And the big deal here is the validation. You cannot have validation with inheritance, just only for your base entity. This reduce the valid use-case to deleting entities, when no payload or special query parameter is expected. |
@NoNameProvided I have thought about the issue and come to the conclusion that I do not want to use this feature, as I would have no way of disabling inherited routes. In my opinion it is better to explicitly state your desired routes in an explicit controller. I think I will create a base class as a construction kit, that can be used by the controller if needed, something like this: https://gist.github.com/enko/26eff8ca7e7528a708d384a6eb39f597 So from my standpoint this would not be needed. |
Stale pull request message |
I just wanted to comment saying that I have created a function similar to what this PR aims to achieve and I have found it extremely useful for reducing code duplication in my routing-controllers/typeorm servers. With this function, you only need to define the entity that you want to operate CRUD operations on, and the rest is generically handled by routing-controllers. Entity:
Create CRUD Controller function:
Usage:
This is just my own way of reducing the code duplication of things that I want to perform simple CRUD operations on. @ivanpadavan Is this what you were looking for? |
@ivanpadavan @dmackenz @enko @alexey-pelykh @closertb @lixaotec @95gabor @pleerock @MichalLytek @jselesan @ssljivic Thank you! |
@jotamorais I would love to finalize this PR, add missing tests. I also find controller inheritance very strong and important feature, it helps reduce duplicative(typical) CRUD operations (as described above) and makes the overall design more flexible. My example: import "reflect-metadata";
import {
JsonController,
UseBefore,
} from "routing-controllers";
import {ValidateJWT} from "../middleware/ValidateJWT";
import {ApiControllerTemplate} from "./ApiContollerTemplate";
import {ProductManager} from "../service/commerce/ProductManager";
import {ProductRepository} from "../repository/ProductRepository";
@UseBefore(ValidateJWT)
@JsonController("/product")
export class ProductController extends ApiControllerTemplate<Product> {
constructor() {
super();
this.domain = "product";
this.cloudManager = new ProductManager();
this.localManager = new ProductRepository();
}
} import {getRepository} from "typeorm";
import {Body, Delete, Get, HttpCode, Param, Post, Put, Res} from "routing-controllers";
import {Authenticator} from "../service/Authenticator";
import {ProductRepository} from "../repository/ProductRepository";
import {CategoryRepository} from "../repository/CategoryRepository";
import {UserRepository} from "../repository/UserRepository";
import {CurrentUser} from "../decorator/CurrentUser";
import {User} from "../entity/User";
import {ProductManager} from "../service/commerce/ProductManager";
import {Product} from "../entity/Product";
import {CategoryManager} from "../service/commerce/CategoryManager";
export class ApiControllerTemplate<T> {
public domain: string;
public cloudManager: CategoryManager | ProductManager | any;
public localManager: CategoryRepository | ProductRepository | any;
protected authenticator: Authenticator;
protected userRepository: UserRepository;
constructor() {
this.authenticator = new Authenticator();
this.userRepository = new UserRepository();
}
@Post()
@HttpCode(201)
public async create(
@CurrentUser() currentUser: User,
@Body() payload: any,
@Res() res: any
) {
try {
const cloud = await this.cloudManager.create(payload);
const local = await this.localManager.create(currentUser, cloud.data.data);
res.status(201);
res.location(this.buildLocationUrl(local.id));
return {};
} catch (e) {
res.status(e.response.status);
return this.buildErrorView(e);
}
}
@Get("/:id")
public async get(
@Param("id") id: number,
@Res() res: any
) {
try {
const local = await getRepository(Product).findOneOrFail(id);
const cloud = await this.cloudManager.get(local.id);
return cloud.data.data;
} catch (e) {
res.status(e.response.status);
return this.buildErrorView(e);
}
}
@Delete("/:id")
public async delete(
@Param("id") id: number,
@Res() res: any
) {
try {
await getRepository(Product).findOneOrFail(id);
await this.cloudManager.delete(id);
return {};
} catch (e) {
res.status(e.response.status);
return this.buildErrorView(e);
}
}
@Put("/:id")
public async edit(
@Param("id") id: number,
@Body() payload: any,
@Res() res: any
) {
try {
await getRepository(Product).findOneOrFail(id);
await this.cloudManager.update(id, payload);
res.status(204);
return {};
} catch (e) {
res.status(e.status);
return this.buildErrorView(e);
}
}
protected buildLocationUrl(id: number): string {
return `/api/${this.domain}/${id}`;
}
protected buildErrorView(e: any) {
if (e.response.data) {
return {
title: e.response.data.title,
errors: e.response.data.errors
};
}
return {};
}
} |
Since the original source repository of this repository no longer exists, I've merged this PR via a patch and it can be tested in the release candidate 0.9.0-alpha.1. |
@jotamorais Hi, thanks for merging the PR, templates, and inheritance will help much in my work. |
Yes, absolutely. I posted a comment in the the issue mentioning that this feature is still lacking test coverage, documentation and examples. |
@jotamorais cool, btw thanks for your quick replies |
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
This PR aims to fix #147.
First of all, thank you for amazing typestack! I've just started my exploration and got a feeling that I found a robust solution for full-stack typescript programming. This solves may be more concerns that I have at the moment, so huge thank you again. Making backend was never so nice and natural.
I found a problem that bothers me - inability to use inheritance without limitations & concerns.I've found it's unnatural and counterintuitive.
Solving this provides an opportunity to create complicated controllers (that simulates OData endpoint, for example) which could be reused easily. Or the definition of simple CRUD controller becomes a one-liner. Which is great, as for me. So solving this also opens doors for contribution on top of the stack.
Best Regards, sorry for my english.