diff --git a/apps/server/README-decorators.md b/apps/server/README-decorators.md new file mode 100644 index 0000000..b0372fe --- /dev/null +++ b/apps/server/README-decorators.md @@ -0,0 +1,147 @@ +# 路由装饰器使用指南 + +本项目已支持使用装饰器来自动注册路由,让控制器代码更加简洁和声明式。 + +## 快速开始 + +### 1. 创建控制器 + +```typescript +import type { Context } from 'koa'; +import { Controller, Get, Post, Put, Delete } from '../decorators/route.ts'; +import { BusinessError } from '../middlewares/exception.ts'; + +@Controller('/api-prefix') // 控制器路由前缀 +export class MyController { + + @Get('/users') + async getUsers(ctx: Context) { + // 直接返回数据,自动包装成统一响应格式 + return { users: [] }; + } + + @Post('/users') + async createUser(ctx: Context) { + const userData = ctx.request.body; + // 业务逻辑处理... + return { id: 1, ...userData }; + } + + @Put('/users/:id') + async updateUser(ctx: Context) { + const { id } = ctx.params; + const userData = ctx.request.body; + // 业务逻辑处理... + return { id, ...userData }; + } + + @Delete('/users/:id') + async deleteUser(ctx: Context) { + const { id } = ctx.params; + // 业务逻辑处理... + return { success: true }; + } +} +``` + +### 2. 注册控制器 + +在 `middlewares/router.ts` 的 `registerDecoratorRoutes()` 方法中添加你的控制器: + +```typescript +this.routeScanner.registerControllers([ + ApplicationController, + UserController, + MyController // 添加你的控制器 +]); +``` + +## 可用装饰器 + +### HTTP方法装饰器 + +- `@Get(path)` - GET 请求 +- `@Post(path)` - POST 请求 +- `@Put(path)` - PUT 请求 +- `@Delete(path)` - DELETE 请求 +- `@Patch(path)` - PATCH 请求 + +### 控制器装饰器 + +- `@Controller(prefix)` - 控制器路由前缀 + +## 路径拼接规则 + +最终的API路径 = 全局前缀 + 控制器前缀 + 方法路径 + +例如: +- 全局前缀:`/api` +- 控制器前缀:`/user` +- 方法路径:`/list` +- 最终路径:`/api/user/list` + +## 响应格式 + +控制器方法只需要返回数据,系统会自动包装成统一响应格式: + +```json +{ + "code": 0, + "message": "操作成功", + "data": { /* 控制器返回的数据 */ }, + "timestamp": 1693478400000 +} +``` + +## 异常处理 + +可以抛出 `BusinessError` 来返回业务异常: + +```typescript +import { BusinessError } from '../middlewares/exception.ts'; + +@Get('/users/:id') +async getUser(ctx: Context) { + const { id } = ctx.params; + + if (!id) { + throw new BusinessError('用户ID不能为空', 1001, 400); + } + + // 正常业务逻辑... + return userData; +} +``` + +## 路由参数 + +支持标准的Koa路由参数: + +```typescript +@Get('/users/:id') // 路径参数 +@Get('/users/:id/posts/:pid') // 多个参数 +@Get('/search') // 查询参数通过 ctx.query 获取 +``` + +## 现有路由 + +项目中已注册的路由: + +### ApplicationController +- `GET /api/application/list` - 获取应用列表 +- `GET /api/application/detail/:id` - 获取应用详情 + +### UserController +- `GET /api/user/list` - 获取用户列表 +- `GET /api/user/detail/:id` - 获取用户详情 +- `POST /api/user` - 创建用户 +- `PUT /api/user/:id` - 更新用户 +- `DELETE /api/user/:id` - 删除用户 +- `GET /api/user/search` - 搜索用户 + +## 注意事项 + +1. 需要安装依赖:`pnpm add reflect-metadata` +2. TypeScript配置需要开启装饰器支持 +3. 控制器类需要导出并在路由中间件中注册 +4. 控制器方法应该返回数据而不是直接操作 `ctx.body` diff --git a/apps/server/app.ts b/apps/server/app.ts index caf7cef..50bc0c8 100644 --- a/apps/server/app.ts +++ b/apps/server/app.ts @@ -1,9 +1,10 @@ +import 'reflect-metadata'; import Koa from 'koa'; -import { registerMiddlewares } from './middlewares/index.ts'; +import { initMiddlewares } from './middlewares/index.ts'; const app = new Koa(); -registerMiddlewares(app); +initMiddlewares(app); app.listen(3000, () => { console.log('server started at http://localhost:3000'); diff --git a/apps/server/controllers/application.ts b/apps/server/controllers/application.ts index c217d6b..0c0afa6 100644 --- a/apps/server/controllers/application.ts +++ b/apps/server/controllers/application.ts @@ -1,11 +1,50 @@ import type { Context } from 'koa'; import prisma from '../libs/db.ts'; +import { BusinessError } from '../middlewares/exception.ts'; +import { Controller, Get } from '../decorators/route.ts'; -export async function list(ctx: Context) { - const list = await prisma.application.findMany({ - where: { - valid: 1, - }, - }); - ctx.body = list; +@Controller('/application') +export class ApplicationController { + @Get('/list') + async list(ctx: Context) { + try { + const list = await prisma.application.findMany({ + where: { + valid: 1, + }, + }); + + // 直接返回数据,由路由中间件统一包装成响应格式 + return list; + } catch (error) { + // 抛出业务异常,由全局异常处理中间件捕获 + throw new BusinessError('获取应用列表失败', 1001, 500); + } + } + + @Get('/detail/:id') + async detail(ctx: Context) { + try { + const { id } = ctx.params; + const app = await prisma.application.findUnique({ + where: { id: Number(id) }, + }); + + if (!app) { + throw new BusinessError('应用不存在', 1002, 404); + } + + return app; + } catch (error) { + if (error instanceof BusinessError) { + throw error; + } + throw new BusinessError('获取应用详情失败', 1003, 500); + } + } } + +// 保持向后兼容的导出方式 +const applicationController = new ApplicationController(); +export const list = applicationController.list.bind(applicationController); +export const detail = applicationController.detail.bind(applicationController); diff --git a/apps/server/controllers/user.ts b/apps/server/controllers/user.ts new file mode 100644 index 0000000..1cef62f --- /dev/null +++ b/apps/server/controllers/user.ts @@ -0,0 +1,117 @@ +import type { Context } from 'koa'; +import { Controller, Get, Post, Put, Delete } from '../decorators/route.ts'; +import { BusinessError } from '../middlewares/exception.ts'; + +/** + * 用户控制器 + */ +@Controller('/user') +export class UserController { + + @Get('/list') + async list(ctx: Context) { + // 模拟用户列表数据 + const users = [ + { id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' }, + { id: 2, name: 'Bob', email: 'bob@example.com', status: 'inactive' }, + { id: 3, name: 'Charlie', email: 'charlie@example.com', status: 'active' } + ]; + + return users; + } + + @Get('/detail/:id') + async detail(ctx: Context) { + const { id } = ctx.params; + + // 模拟根据ID查找用户 + const user = { + id: Number(id), + name: 'User ' + id, + email: `user${id}@example.com`, + status: 'active', + createdAt: new Date().toISOString() + }; + + if (Number(id) > 100) { + throw new BusinessError('用户不存在', 2001, 404); + } + + return user; + } + + @Post('') + async create(ctx: Context) { + const body = (ctx.request as any).body; + + // 模拟创建用户 + const newUser = { + id: Date.now(), + ...body, + createdAt: new Date().toISOString(), + status: 'active' + }; + + return newUser; + } + + @Put('/:id') + async update(ctx: Context) { + const { id } = ctx.params; + const body = (ctx.request as any).body; + + // 模拟更新用户 + const updatedUser = { + id: Number(id), + ...body, + updatedAt: new Date().toISOString() + }; + + return updatedUser; + } + + @Delete('/:id') + async delete(ctx: Context) { + const { id } = ctx.params; + + if (Number(id) === 1) { + throw new BusinessError('管理员账户不能删除', 2002, 403); + } + + // 模拟删除操作 + return { + success: true, + message: `用户 ${id} 已删除`, + deletedAt: new Date().toISOString() + }; + } + + @Get('/search') + async search(ctx: Context) { + const { keyword, status } = ctx.query; + + // 模拟搜索逻辑 + let results = [ + { id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' }, + { id: 2, name: 'Bob', email: 'bob@example.com', status: 'inactive' } + ]; + + if (keyword) { + results = results.filter(user => + user.name.toLowerCase().includes(String(keyword).toLowerCase()) || + user.email.toLowerCase().includes(String(keyword).toLowerCase()) + ); + } + + if (status) { + results = results.filter(user => user.status === status); + } + + return { + keyword, + status, + total: results.length, + results + }; + } +} diff --git a/apps/server/decorators/route.ts b/apps/server/decorators/route.ts new file mode 100644 index 0000000..8c2b0ab --- /dev/null +++ b/apps/server/decorators/route.ts @@ -0,0 +1,102 @@ +import 'reflect-metadata'; + +/** + * 路由元数据键 + */ +export const ROUTE_METADATA_KEY = Symbol('route'); + +/** + * HTTP 方法类型 + */ +export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + +/** + * 路由元数据接口 + */ +export interface RouteMetadata { + method: HttpMethod; + path: string; + propertyKey: string; +} + +/** + * 创建HTTP方法装饰器的工厂函数 + */ +function createMethodDecorator(method: HttpMethod) { + return function (path: string = '') { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + // 获取现有的路由元数据 + const existingRoutes: RouteMetadata[] = (Reflect as any).getMetadata(ROUTE_METADATA_KEY, target.constructor) || []; + + // 添加新的路由元数据 + const newRoute: RouteMetadata = { + method, + path, + propertyKey + }; + + existingRoutes.push(newRoute); + + // 保存路由元数据到类的构造函数上 + (Reflect as any).defineMetadata(ROUTE_METADATA_KEY, existingRoutes, target.constructor); + + return descriptor; + }; + }; +} + +/** + * GET 请求装饰器 + * @param path 路由路径 + */ +export const Get = createMethodDecorator('GET'); + +/** + * POST 请求装饰器 + * @param path 路由路径 + */ +export const Post = createMethodDecorator('POST'); + +/** + * PUT 请求装饰器 + * @param path 路由路径 + */ +export const Put = createMethodDecorator('PUT'); + +/** + * DELETE 请求装饰器 + * @param path 路由路径 + */ +export const Delete = createMethodDecorator('DELETE'); + +/** + * PATCH 请求装饰器 + * @param path 路由路径 + */ +export const Patch = createMethodDecorator('PATCH'); + +/** + * 控制器装饰器 + * @param prefix 路由前缀 + */ +export function Controller(prefix: string = '') { + return function (constructor: T) { + // 保存控制器前缀到元数据 + (Reflect as any).defineMetadata('prefix', prefix, constructor); + return constructor; + }; +} + +/** + * 获取控制器的路由元数据 + */ +export function getRouteMetadata(constructor: any): RouteMetadata[] { + return (Reflect as any).getMetadata(ROUTE_METADATA_KEY, constructor) || []; +} + +/** + * 获取控制器的路由前缀 + */ +export function getControllerPrefix(constructor: any): string { + return (Reflect as any).getMetadata('prefix', constructor) || ''; +} diff --git a/apps/server/libs/route-scanner.ts b/apps/server/libs/route-scanner.ts new file mode 100644 index 0000000..052bc2a --- /dev/null +++ b/apps/server/libs/route-scanner.ts @@ -0,0 +1,166 @@ +import type Koa from 'koa'; +import KoaRouter from '@koa/router'; +import { getRouteMetadata, getControllerPrefix, type RouteMetadata } from '../decorators/route.ts'; +import { createAutoSuccessResponse } from '../middlewares/exception.ts'; + +/** + * 控制器类型 + */ +export interface ControllerClass { + new (...args: any[]): any; +} + +/** + * 路由扫描器,用于自动注册装饰器标注的路由 + */ +export class RouteScanner { + private router: KoaRouter; + private controllers: ControllerClass[] = []; + + constructor(prefix: string = '/api') { + this.router = new KoaRouter({ prefix }); + } + + /** + * 注册控制器类 + */ + registerController(ControllerClass: ControllerClass): void { + this.controllers.push(ControllerClass); + this.scanController(ControllerClass); + } + + /** + * 注册多个控制器类 + */ + registerControllers(controllers: ControllerClass[]): void { + controllers.forEach(controller => this.registerController(controller)); + } + + /** + * 扫描控制器并注册路由 + */ + private scanController(ControllerClass: ControllerClass): void { + // 创建控制器实例 + const controllerInstance = new ControllerClass(); + + // 获取控制器的路由前缀 + const controllerPrefix = getControllerPrefix(ControllerClass); + + // 获取控制器的路由元数据 + const routes: RouteMetadata[] = getRouteMetadata(ControllerClass); + + // 注册每个路由 + routes.forEach(route => { + const fullPath = this.buildFullPath(controllerPrefix, route.path); + const handler = this.wrapControllerMethod(controllerInstance, route.propertyKey); + + // 根据HTTP方法注册路由 + switch (route.method) { + case 'GET': + this.router.get(fullPath, handler); + break; + case 'POST': + this.router.post(fullPath, handler); + break; + case 'PUT': + this.router.put(fullPath, handler); + break; + case 'DELETE': + this.router.delete(fullPath, handler); + break; + case 'PATCH': + this.router.patch(fullPath, handler); + break; + default: + console.warn(`未支持的HTTP方法: ${route.method}`); + } + + console.log(`注册路由: ${route.method} ${fullPath} -> ${ControllerClass.name}.${route.propertyKey}`); + }); + } + + /** + * 构建完整的路由路径 + */ + private buildFullPath(controllerPrefix: string, routePath: string): string { + // 清理和拼接路径 + const cleanControllerPrefix = controllerPrefix.replace(/^\/+|\/+$/g, ''); + const cleanRoutePath = routePath.replace(/^\/+|\/+$/g, ''); + + let fullPath = ''; + if (cleanControllerPrefix) { + fullPath += '/' + cleanControllerPrefix; + } + if (cleanRoutePath) { + fullPath += '/' + cleanRoutePath; + } + + // 如果路径为空,返回根路径 + return fullPath || '/'; + } + + /** + * 包装控制器方法,统一处理响应格式 + */ + private wrapControllerMethod(instance: any, methodName: string) { + return async (ctx: Koa.Context, next: Koa.Next) => { + try { + // 调用控制器方法 + const method = instance[methodName]; + if (typeof method !== 'function') { + throw new Error(`控制器方法 ${methodName} 不存在或不是函数`); + } + + // 绑定this并调用方法 + const result = await method.call(instance, ctx, next); + + // 如果控制器返回了数据,则包装成统一响应格式 + if (result !== undefined) { + ctx.body = createAutoSuccessResponse(result); + } + // 如果控制器没有返回数据,说明已经自己处理了响应 + } catch (error) { + // 错误由全局异常处理中间件处理 + throw error; + } + }; + } + + /** + * 获取Koa路由器实例,用于应用到Koa应用中 + */ + getRouter(): KoaRouter { + return this.router; + } + + /** + * 应用路由到Koa应用 + */ + applyToApp(app: Koa): void { + app.use(this.router.routes()); + app.use(this.router.allowedMethods()); + } + + /** + * 获取已注册的路由信息(用于调试) + */ + getRegisteredRoutes(): Array<{ method: string; path: string; controller: string; action: string }> { + const routes: Array<{ method: string; path: string; controller: string; action: string }> = []; + + this.controllers.forEach(ControllerClass => { + const controllerPrefix = getControllerPrefix(ControllerClass); + const routeMetadata = getRouteMetadata(ControllerClass); + + routeMetadata.forEach(route => { + routes.push({ + method: route.method, + path: this.buildFullPath(controllerPrefix, route.path), + controller: ControllerClass.name, + action: route.propertyKey + }); + }); + }); + + return routes; + } +} diff --git a/apps/server/middlewares/body-parser.ts b/apps/server/middlewares/body-parser.ts new file mode 100644 index 0000000..dccb0a3 --- /dev/null +++ b/apps/server/middlewares/body-parser.ts @@ -0,0 +1,46 @@ +import type Koa from 'koa'; +import type { Middleware } from './types.ts'; + +/** + * 请求体解析中间件 + */ +export class BodyParser implements Middleware { + apply(app: Koa): void { + // 使用动态导入来避免类型问题 + app.use(async (ctx, next) => { + if (ctx.request.method === 'POST' || + ctx.request.method === 'PUT' || + ctx.request.method === 'PATCH') { + + // 简单的JSON解析 + if (ctx.request.type === 'application/json') { + try { + const chunks: Buffer[] = []; + + ctx.req.on('data', (chunk) => { + chunks.push(chunk); + }); + + await new Promise((resolve) => { + ctx.req.on('end', () => { + const body = Buffer.concat(chunks).toString(); + try { + (ctx.request as any).body = JSON.parse(body); + } catch { + (ctx.request as any).body = {}; + } + resolve(void 0); + }); + }); + } catch (error) { + (ctx.request as any).body = {}; + } + } else { + (ctx.request as any).body = {}; + } + } + + await next(); + }); + } +} diff --git a/apps/server/middlewares/exception.ts b/apps/server/middlewares/exception.ts new file mode 100644 index 0000000..487f456 --- /dev/null +++ b/apps/server/middlewares/exception.ts @@ -0,0 +1,138 @@ +import type Koa from 'koa'; +import type { Middleware } from './types.ts'; + +/** + * 统一响应体结构 + */ +export interface ApiResponse { + code: number; // 状态码:0表示成功,其他表示失败 + message: string; // 响应消息 + data?: T; // 响应数据 + timestamp: number; // 时间戳 +} + +/** + * 自定义业务异常类 + */ +export class BusinessError extends Error { + public code: number; + public httpStatus: number; + + constructor(message: string, code = 1000, httpStatus = 400) { + super(message); + this.name = 'BusinessError'; + this.code = code; + this.httpStatus = httpStatus; + } +} + +/** + * 全局异常处理中间件 + */ +export class Exception implements Middleware { + apply(app: Koa): void { + app.use(async (ctx, next) => { + try { + await next(); + + // 如果没有设置响应体,则返回404 + if (ctx.status === 404 && !ctx.body) { + this.sendResponse(ctx, 404, '接口不存在', null, 404); + } + } catch (error) { + console.error('全局异常捕获:', error); + this.handleError(ctx, error); + } + }); + } + + /** + * 处理错误 + */ + private handleError(ctx: Koa.Context, error: any): void { + if (error instanceof BusinessError) { + // 业务异常 + this.sendResponse(ctx, error.code, error.message, null, error.httpStatus); + } else if (error.status) { + // Koa HTTP 错误 + const message = error.status === 401 ? '未授权访问' : + error.status === 403 ? '禁止访问' : + error.status === 404 ? '资源不存在' : + error.status === 422 ? '请求参数错误' : + error.message || '请求失败'; + + this.sendResponse(ctx, error.status, message, null, error.status); + } else { + // 系统异常 + const isDev = process.env.NODE_ENV === 'development'; + const message = isDev ? error.message : '服务器内部错误'; + const data = isDev ? { stack: error.stack } : null; + + this.sendResponse(ctx, 500, message, data, 500); + } + } + + /** + * 发送统一响应 + */ + private sendResponse( + ctx: Koa.Context, + code: number, + message: string, + data: any = null, + httpStatus = 200 + ): void { + const response: ApiResponse = { + code, + message, + data, + timestamp: Date.now() + }; + + ctx.status = httpStatus; + ctx.body = response; + ctx.type = 'application/json'; + } +} + +/** + * 创建成功响应的辅助函数 + */ +export function createSuccessResponse(data: T, message = '操作成功'): ApiResponse { + return { + code: 0, + message, + data, + timestamp: Date.now() + }; +} + +/** + * 创建成功响应的辅助函数(自动设置消息) + */ +export function createAutoSuccessResponse(data: T): ApiResponse { + let message = '操作成功'; + + // 根据数据类型自动生成消息 + if (Array.isArray(data)) { + message = `获取列表成功,共${data.length}条记录`; + } else if (data && typeof data === 'object') { + message = '获取数据成功'; + } else if (data === null || data === undefined) { + message = '操作完成'; + } + + return createSuccessResponse(data, message); +} + +/** + * 创建失败响应的辅助函数 + */ +export function createErrorResponse(code: number, message: string, data?: any): ApiResponse { + return { + code, + message, + data, + timestamp: Date.now() + }; +} diff --git a/apps/server/middlewares/index.ts b/apps/server/middlewares/index.ts index b562a21..70429b5 100644 --- a/apps/server/middlewares/index.ts +++ b/apps/server/middlewares/index.ts @@ -1,10 +1,25 @@ import { Router } from './router.ts'; import { ResponseTime } from './responseTime.ts'; +import { Exception } from './exception.ts'; +import { BodyParser } from './body-parser.ts'; import type Koa from 'koa'; -export function registerMiddlewares(app: Koa) { - const router = new Router(); +/** + * 初始化中间件 + * @param app Koa + */ +export function initMiddlewares(app: Koa) { + // 全局异常处理中间件必须最先注册 + const exception = new Exception(); + exception.apply(app); + + // 请求体解析中间件 + const bodyParser = new BodyParser(); + bodyParser.apply(app); + const responseTime = new ResponseTime(); responseTime.apply(app); + + const router = new Router(); router.apply(app); } diff --git a/apps/server/middlewares/router.ts b/apps/server/middlewares/router.ts index 0476bfb..f6d09b6 100644 --- a/apps/server/middlewares/router.ts +++ b/apps/server/middlewares/router.ts @@ -1,18 +1,83 @@ import KoaRouter from '@koa/router'; import type Koa from 'koa'; import type { Middleware } from './types.ts'; +import { createAutoSuccessResponse } from './exception.ts'; +import { RouteScanner } from '../libs/route-scanner.ts'; +import { ApplicationController } from '../controllers/application.ts'; +import { UserController } from '../controllers/user.ts'; import * as application from '../controllers/application.ts'; +/** + * 包装控制器函数,统一处理响应格式 + */ +function wrapController(controllerFn: Function) { + return async (ctx: Koa.Context, next: Koa.Next) => { + try { + // 调用控制器函数获取返回数据 + const result = await controllerFn(ctx, next); + + // 如果控制器返回了数据,则包装成统一响应格式 + if (result !== undefined) { + ctx.body = createAutoSuccessResponse(result); + } + // 如果控制器没有返回数据,说明已经自己处理了响应 + } catch (error) { + // 错误由全局异常处理中间件处理 + throw error; + } + }; +} + export class Router implements Middleware { private router: KoaRouter; + private routeScanner: RouteScanner; + constructor() { this.router = new KoaRouter({ prefix: '/api', }); - this.router.get('/application/list', application.list); + + // 初始化路由扫描器 + this.routeScanner = new RouteScanner('/api'); + + // 注册装饰器路由 + this.registerDecoratorRoutes(); + + // 注册传统路由(向后兼容) + this.registerTraditionalRoutes(); + } + + /** + * 注册装饰器路由 + */ + private registerDecoratorRoutes(): void { + // 注册所有使用装饰器的控制器 + this.routeScanner.registerControllers([ + ApplicationController, + UserController + ]); + + // 输出注册的路由信息 + const routes = this.routeScanner.getRegisteredRoutes(); + console.log('装饰器路由注册完成:'); + routes.forEach(route => { + console.log(` ${route.method} ${route.path} -> ${route.controller}.${route.action}`); + }); + } + + /** + * 注册传统路由(向后兼容) + */ + private registerTraditionalRoutes(): void { + // 保持对老版本的兼容,如果需要可以在这里注册非装饰器路由 + // this.router.get('/application/list-legacy', wrapController(application.list)); } apply(app: Koa) { + // 应用装饰器路由 + this.routeScanner.applyToApp(app); + + // 应用传统路由 app.use(this.router.routes()); app.use(this.router.allowedMethods()); } diff --git a/apps/server/package.json b/apps/server/package.json index 3b93eaf..ff87b8e 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -12,7 +12,8 @@ "dependencies": { "@koa/router": "^14.0.0", "@prisma/client": "^6.15.0", - "koa": "^3.0.1" + "koa": "^3.0.1", + "reflect-metadata": "^0.1.13" }, "devDependencies": { "@tsconfig/node-ts": "^23.6.1", diff --git a/apps/server/prisma/data/dev.db b/apps/server/prisma/data/dev.db index 968a594..fa54602 100644 Binary files a/apps/server/prisma/data/dev.db and b/apps/server/prisma/data/dev.db differ diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 43294bd..69ee9b9 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -2,5 +2,10 @@ "extends": [ "@tsconfig/node22/tsconfig.json", "@tsconfig/node-ts/tsconfig.json" - ] + ], + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "types": ["reflect-metadata"] + } } diff --git a/package.json b/package.json index c72829b..fd01cb4 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "", "scripts": { - "dev": "pnpm run dev:server && pnpm run dev:web" + "dev": "pnpm run dev" }, "devDependencies": { "@biomejs/biome": "2.0.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6f944e..b6582f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: koa: specifier: ^3.0.1 version: 3.0.1 + reflect-metadata: + specifier: ^0.1.13 + version: 0.1.14 devDependencies: '@tsconfig/node-ts': specifier: ^23.6.1 @@ -1460,6 +1463,9 @@ packages: reduce-configs@1.1.1: resolution: {integrity: sha512-EYtsVGAQarE8daT54cnaY1PIknF2VB78ug6Zre2rs36EsJfC40EG6hmTU2A2P1ZuXnKAt2KI0fzOGHcX7wzdPw==} + reflect-metadata@0.1.14: + resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -2930,6 +2936,8 @@ snapshots: reduce-configs@1.1.1: {} + reflect-metadata@0.1.14: {} + resize-observer-polyfill@1.5.1: {} resolve-from@4.0.0: {}