Skip to content
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
4 changes: 4 additions & 0 deletions packages/decorators/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
build/*
dist/*
out/*
10 changes: 10 additions & 0 deletions packages/decorators/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
};

99 changes: 99 additions & 0 deletions packages/decorators/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# @plane/decorators

A lightweight TypeScript decorator library for building Express.js controllers with a clean, declarative syntax.

## Features

- TypeScript-first design
- Decorators for HTTP methods (GET, POST, PUT, PATCH, DELETE)
- WebSocket support
- Middleware support
- No build step required - works directly with TypeScript files

## Installation

This package is part of the Plane workspace and can be used by adding it to your project's dependencies:

```json
{
"dependencies": {
"@plane/decorators": "*"
}
}
```

## Usage

### Basic REST Controller

```typescript
import { Controller, Get, Post, BaseController } from "@plane/decorators";
import { Router, Request, Response } from "express";

@Controller("/api/users")
class UserController extends BaseController {
@Get("/")
async getUsers(req: Request, res: Response) {
return res.json({ users: [] });
}

@Post("/")
async createUser(req: Request, res: Response) {
return res.json({ success: true });
}
}

// Register routes
const router = Router();
const userController = new UserController();
userController.registerRoutes(router);
```

### WebSocket Controller

```typescript
import {
Controller,
WebSocket,
BaseWebSocketController,
} from "@plane/decorators";
import { Request } from "express";
import { WebSocket as WS } from "ws";

@Controller("/ws/chat")
class ChatController extends BaseWebSocketController {
@WebSocket("/")
handleConnection(ws: WS, req: Request) {
ws.on("message", (message) => {
ws.send(`Received: ${message}`);
});
}
}

// Register WebSocket routes
const router = require("express-ws")(app).router;
const chatController = new ChatController();
chatController.registerWebSocketRoutes(router);
```

## API Reference

### Decorators

- `@Controller(baseRoute: string)` - Class decorator for defining a base route
- `@Get(route: string)` - Method decorator for HTTP GET endpoints
- `@Post(route: string)` - Method decorator for HTTP POST endpoints
- `@Put(route: string)` - Method decorator for HTTP PUT endpoints
- `@Patch(route: string)` - Method decorator for HTTP PATCH endpoints
- `@Delete(route: string)` - Method decorator for HTTP DELETE endpoints
- `@WebSocket(route: string)` - Method decorator for WebSocket endpoints
- `@Middleware(middleware: RequestHandler)` - Method decorator for applying middleware

### Classes

- `BaseController` - Base class for REST controllers
- `BaseWebSocketController` - Base class for WebSocket controllers

## License

This project is licensed under the [GNU Affero General Public License v3.0](https://github.com/makeplane/plane/blob/master/LICENSE.txt).
42 changes: 42 additions & 0 deletions packages/decorators/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@plane/decorators",
"version": "0.1.0",
"description": "Controller and route decorators for Express.js applications",
"license": "AGPL-3.0",
"private": true,
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist/**"
],
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts --external express,ws",
"dev": "tsup src/index.ts --format esm,cjs --watch --dts --external express,ws",
"lint": "eslint src --ext .ts,.tsx",
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"dependencies": {
"reflect-metadata": "^0.2.2",
"express": "^4.21.2"
},
"devDependencies": {
"@plane/eslint-config": "*",
"@types/express": "^4.17.21",
"@types/reflect-metadata": "^0.1.0",
"@plane/typescript-config": "*",
"@types/node": "^20.14.9",
"@types/ws": "^8.5.10",
"tsup": "8.3.0",
"typescript": "^5.3.3"
},
"peerDependencies": {
"express": ">=4.21.2",
"ws": ">=8.0.0"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
}
}
}
61 changes: 61 additions & 0 deletions packages/decorators/src/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { RequestHandler, Router } from "express";
import "reflect-metadata";

type HttpMethod =
| "get"
| "post"
| "put"
| "delete"
| "patch"
| "options"
| "head"
| "ws";

interface ControllerInstance {
[key: string]: unknown;
}

interface ControllerConstructor {
new (...args: any[]): ControllerInstance;
prototype: ControllerInstance;
}

export function registerControllers(
router: Router,
Controller: ControllerConstructor,
): void {
const instance = new Controller();
const baseRoute = Reflect.getMetadata("baseRoute", Controller) as string;

Object.getOwnPropertyNames(Controller.prototype).forEach((methodName) => {
if (methodName === "constructor") return; // Skip the constructor

const method = Reflect.getMetadata(
"method",
instance,
methodName,
) as HttpMethod;
const route = Reflect.getMetadata("route", instance, methodName) as string;
const middlewares =
(Reflect.getMetadata(
"middlewares",
instance,
methodName,
) as RequestHandler[]) || [];

if (method && route) {
const handler = instance[methodName] as unknown;

if (typeof handler === "function") {
if (method !== "ws") {
(
router[method] as (
path: string,
...handlers: RequestHandler[]
) => void
)(`${baseRoute}${route}`, ...middlewares, handler.bind(instance));
}
}
}
});
}
15 changes: 15 additions & 0 deletions packages/decorators/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Export individual decorators
export { Controller, Middleware } from "./rest";
export { Get, Post, Put, Patch, Delete } from "./rest";
export { WebSocket } from "./websocket";
export { registerControllers } from "./controller";
export { registerWebSocketControllers } from "./websocket-controller";

// Also provide namespaced exports for better organization
import * as RestDecorators from "./rest";
import * as WebSocketDecorators from "./websocket";

// Named namespace exports
export const Rest = RestDecorators;
export const WebSocketNS = WebSocketDecorators;

61 changes: 61 additions & 0 deletions packages/decorators/src/rest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import "reflect-metadata";
import { RequestHandler } from "express";

// Define valid HTTP methods
type RestMethod = "get" | "post" | "put" | "patch" | "delete";

/**
* Controller decorator
* @param baseRoute
* @returns
*/
export function Controller(baseRoute: string = ""): ClassDecorator {
return function (target: Function) {
Reflect.defineMetadata("baseRoute", baseRoute, target);
};
}

/**
* Factory function to create HTTP method decorators
* @param method HTTP method to handle
* @returns Method decorator
*/
function createHttpMethodDecorator(
method: RestMethod
): (route: string) => MethodDecorator {
return function (route: string): MethodDecorator {
return function (
target: object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) {
Reflect.defineMetadata("method", method, target, propertyKey);
Reflect.defineMetadata("route", route, target, propertyKey);
};
};
}

// Export HTTP method decorators using the factory
export const Get = createHttpMethodDecorator("get");
export const Post = createHttpMethodDecorator("post");
export const Put = createHttpMethodDecorator("put");
export const Patch = createHttpMethodDecorator("patch");
export const Delete = createHttpMethodDecorator("delete");

/**
* Middleware decorator
* @param middleware
* @returns
*/
export function Middleware(middleware: RequestHandler): MethodDecorator {
return function (
target: object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor,
) {
const middlewares =
Reflect.getMetadata("middlewares", target, propertyKey) || [];
middlewares.push(middleware);
Reflect.defineMetadata("middlewares", middlewares, target, propertyKey);
};
}
85 changes: 85 additions & 0 deletions packages/decorators/src/websocket-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Router, Request } from "express";
import type { WebSocket } from "ws";
import "reflect-metadata";

interface ControllerInstance {
[key: string]: unknown;
}

interface ControllerConstructor {
new (...args: any[]): ControllerInstance;
prototype: ControllerInstance;
}

export function registerWebSocketControllers(
router: Router,
Controller: ControllerConstructor,
existingInstance?: ControllerInstance,
): void {
const instance = existingInstance || new Controller();
const baseRoute = Reflect.getMetadata("baseRoute", Controller) as string;

Object.getOwnPropertyNames(Controller.prototype).forEach((methodName) => {
if (methodName === "constructor") return; // Skip the constructor

const method = Reflect.getMetadata(
"method",
instance,
methodName,
) as string;
const route = Reflect.getMetadata("route", instance, methodName) as string;

if (method === "ws" && route) {
const handler = instance[methodName] as unknown;

if (
typeof handler === "function" &&
typeof (router as any).ws === "function"
) {
(router as any).ws(
`${baseRoute}${route}`,
(ws: WebSocket, req: Request) => {
try {
handler.call(instance, ws, req);
} catch (error) {
console.error(
`WebSocket error in ${Controller.name}.${methodName}`,
error,
);
ws.close(
1011,
error instanceof Error
? error.message
: "Internal server error",
);
}
},
);
}
Comment on lines +35 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Type the WebSocket registration to avoid using (router as any).
It appears you rely on express-ws or a similar library that adds .ws to the router. Consider declaring a compatible type instead of casting to any, to ensure better type safety and autocompletion.

}
});
}

/**
* Base controller class for WebSocket endpoints
*/
export abstract class BaseWebSocketController {
protected router: Router;

constructor() {
this.router = Router();
}

/**
* Get the base route for this controller
*/
protected getBaseRoute(): string {
return Reflect.getMetadata("baseRoute", this.constructor) || "";
}

/**
* Abstract method to handle WebSocket connections
* Implement this in your derived class
*/
abstract handleConnection(ws: WebSocket, req: Request): void;
}
Loading