Skip to content

refactor: typescript 5 decorator #5

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

Merged
merged 3 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/src/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import TaskController from './task.controller';

export const controllers = [TaskController];
117 changes: 59 additions & 58 deletions backend/src/controllers/task.controller.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,72 @@
import { NextFunction, Request, Response } from 'express';
import TaskModel, { TaskType } from '../models/task.model';
import { Controller, Delete, Get, Post, Put } from '../utils/decorators/controller.decorator';

const taskModel = new TaskModel();
@Controller('/task')
export default class TaskController {
private taskModel = new TaskModel();

export const getAllTask = async (
req: Request,
res: Response<TaskType[]>,
next: NextFunction
) => {
try {
const tasks = await taskModel.getAll();
res.status(200).json(tasks);
} catch (error) {
next(error);
@Get()
async getAllTask(req: Request, res: Response<TaskType[]>, next: NextFunction) {
try {
const tasks = await this.taskModel.getAll();
res.status(200).json(tasks);
} catch (error) {
next(error);
}
}
};

export const getTaskById = async (
req: Request<{ id: string }>,
res: Response<TaskType | null>,
next: NextFunction
) => {
try {
const task = await taskModel.getById(req.params.id);
res.status(200).json(task);
} catch (error) {
next(error);
@Get('/:id')
async getTaskById(
req: Request<{ id: string }>,
res: Response<TaskType | null>,
next: NextFunction
) {
try {
const task = await this.taskModel.getById(req.params.id);
res.status(200).json(task);
} catch (error) {
next(error);
}
}
};

export const createTask = async (
req: Request<{}, {}, TaskType>,
res: Response<{ id: string }>,
next: NextFunction
) => {
try {
const task = taskModel.validateData(req.body);
const id = await taskModel.create(task);
res.status(201).json({ id });
} catch (error) {
next(error);
@Post()
async createTask(
req: Request<{}, {}, TaskType>,
res: Response<{ id: string }>,
next: NextFunction
) {
try {
const task = this.taskModel.validateData(req.body);
const id = await this.taskModel.create(task);
res.status(201).json({ id });
} catch (error) {
next(error);
}
}
};

export const updateTask = async (
req: Request<{ id: string }, {}, TaskType>,
res: Response,
next: NextFunction
) => {
try {
const task = taskModel.validateData(req.body);
await taskModel.update(req.params.id, task);
res.status(200);
} catch (error) {
next(error);
@Put('/:id')
async updateTask(
req: Request<{ id: string }, {}, TaskType>,
res: Response,
next: NextFunction
) {
try {
const task = this.taskModel.validateData(req.body);
await this.taskModel.update(req.params.id, task);
res.status(200);
} catch (error) {
next(error);
}
}
};

export const deleteTask = async (
req: Request<{ id: string }>,
res: Response,
next: NextFunction
) => {
try {
await taskModel.delete(req.params.id);
res.status(200);
} catch (error) {
next(error);
@Delete('/:id')
async deleteTask(req: Request<{ id: string }>, res: Response, next: NextFunction) {
try {
await this.taskModel.delete(req.params.id);
res.status(200);
} catch (error) {
next(error);
}
}
};
}
32 changes: 27 additions & 5 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
import express, { NextFunction, Request, Response } from 'express';
import express, { NextFunction, Request, Response, Router } from 'express';
import { ZodError } from 'zod';
import taskRouter from './routes/task.route';
import { controllers } from './controllers';
import { validateMetadata } from './utils/decorators/controller.decorator';

const port = process.env.PORT || 9453;

class Server {
private app = express();

start() {
this.app.use(express.json());
private registerRoutes() {
controllers.forEach((Controller) => {
const controller = new Controller();
const { basePath, routers } = validateMetadata(Controller);
const router = Router();

routers.forEach(({ method, path, handlerName }) => {
router[method](
path,
controller[handlerName as keyof typeof controller].bind(controller)
);
});

this.app.use('/task', taskRouter);
this.app.use(basePath, router);
});
}

private errorMiddleware() {
this.app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
if (err instanceof ZodError) {
res.status(400).json({ message: err.errors });
} else {
res.status(500).json({ message: err.message });
}
});
}

start() {
this.app.use(express.json());

this.registerRoutes();

this.errorMiddleware();

this.app.listen(port, () => {
console.log(`Server is running on port ${port}`);
Expand Down
16 changes: 0 additions & 16 deletions backend/src/routes/task.route.ts

This file was deleted.

59 changes: 59 additions & 0 deletions backend/src/utils/decorators/controller.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { z } from 'zod';
import './symbol.polyfill';

enum MetadataKeys {
BASE_PATH = 'basePath',
ROUTERS = 'routers',
}

enum Method {
GET = 'get',
POST = 'post',
PUT = 'put',
DELETE = 'delete',
}

const routerConfigSchema = z.object({
method: z.enum([Method.GET, Method.POST, Method.PUT, Method.DELETE]),
path: z.string(),
handlerName: z.string().or(z.symbol()),
});

const metadataSchema = z.object({
[MetadataKeys.BASE_PATH]: z.string(),
[MetadataKeys.ROUTERS]: z.array(routerConfigSchema),
});

export const validateMetadata = <T extends new (...args: any[]) => {}>(target: T) => {
return metadataSchema.parse(target[Symbol.metadata]);
};

export const Controller = (basePath: string) => {
return (_: new (...args: any[]) => {}, ctx: ClassDecoratorContext) => {
ctx.metadata[MetadataKeys.BASE_PATH] = basePath;
};
};

const methodDecoratorFactory = (method: Method) => {
return (path: string = '') => {
return (_: Function, ctx: ClassMethodDecoratorContext) => {
const metadataRouters = ctx.metadata[MetadataKeys.ROUTERS];
const routers: z.infer<typeof routerConfigSchema>[] = Array.isArray(metadataRouters)
? metadataRouters
: [];

routers.push({
method,
path,
handlerName: ctx.name,
});

ctx.metadata[MetadataKeys.ROUTERS] = routers;
};
};
};

export const Get = methodDecoratorFactory(Method.GET);
export const Post = methodDecoratorFactory(Method.POST);
export const Put = methodDecoratorFactory(Method.PUT);
export const Delete = methodDecoratorFactory(Method.DELETE);
2 changes: 2 additions & 0 deletions backend/src/utils/decorators/symbol.polyfill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// @see https://github.com/microsoft/TypeScript/issues/53461#issuecomment-1606684134
(Symbol as { metadata: symbol }).metadata ??= Symbol("Symbol.metadata");
7 changes: 4 additions & 3 deletions backend/thunder-client.http
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
@url = http://localhost:9453
@id = c3c1dd84-aa3e-4413-b800-448cdbca7034

GET {{url}}/task

###

GET {{url}}/task/2
GET {{url}}/task/{{id}}

###

Expand All @@ -20,7 +21,7 @@ Content-Type: application/json

###

PUT {{url}}/task/2
PUT {{url}}/task/{{id}}
Content-Type: application/json

{
Expand All @@ -32,4 +33,4 @@ Content-Type: application/json

###

DELETE {{url}}/task/2
DELETE {{url}}/task/{{id}}
2 changes: 1 addition & 1 deletion backend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

/* Language and Environment */
"target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
"lib": ["ESNext.Decorators"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
Expand Down