Merge branch 'fix/live-server-restructuring' into fix/live-server-restructuring

This commit is contained in:
Palanikannan M
2025-03-25 19:46:58 +05:30
parent 2db9d35678
commit 93066ef5d5
10 changed files with 59 additions and 172 deletions
+2
View File
@@ -85,3 +85,5 @@ deploy/selfhost/plane-app/
## Storybook
*storybook.log
output.css
dev-editor
@@ -1,21 +1,18 @@
import type { Request } from "express";
import type { WebSocket as WS } from "ws";
import type { Hocuspocus } from "@hocuspocus/server";
import { Controller, WebSocket } from "@/lib/decorators";
import { BaseWebSocketController } from "@/lib/base.controller";
import { ErrorCategory } from "@/core/helpers/error-handling/error-handler";
import { logger } from "@plane/logger";
import Errors from "@/core/helpers/error-handling/error-factory";
import { Controller, WebSocket } from "@plane/decorators";
@Controller("/collaboration")
export class CollaborationController extends BaseWebSocketController {
export class CollaborationController {
private metrics = {
errors: 0,
};
constructor(private readonly hocusPocusServer: Hocuspocus) {
super();
}
constructor(private readonly hocusPocusServer: Hocuspocus) {}
@WebSocket("/")
handleConnection(ws: WS, req: Request) {
+5 -5
View File
@@ -3,17 +3,17 @@ import type { Request, Response } from "express";
import { convertHTMLDocumentToAllFormats } from "@/core/helpers/convert-document";
// types
import { TConvertDocumentRequestBody } from "@/core/types/common";
// controller
import { BaseController } from "@/lib/base.controller";
// decorators
import { Post, CatchErrors } from "@/lib/decorators";
import { CatchErrors } from "@/lib/decorators";
// logger
import { logger } from "@plane/logger";
// error handling
import validate from "@/core/helpers/error-handling/error-validation";
import { Controller, Post } from "@plane/decorators";
export class DocumentController extends BaseController {
@Post("/convert-document")
@Controller("/convert-document")
export class DocumentController {
@Post("/")
@CatchErrors()
async convertDocument(req: Request, res: Response) {
const { description_html, variant } = req.body as TConvertDocumentRequestBody;
+5 -5
View File
@@ -1,16 +1,16 @@
import { CatchErrors } from "@/lib/decorators";
import { Controller, Get } from "@plane/decorators";
import type { Request, Response } from "express";
import { Controller, Get, CatchErrors } from "@/lib/decorators";
import { BaseController } from "@/lib/base.controller";
@Controller("/health")
export class HealthController extends BaseController {
export class HealthController {
@Get("/")
@CatchErrors()
async healthCheck(_req: Request, res: Response) {
res.status(200).json({
res.status(200).json({
status: "OK",
timestamp: new Date().toISOString(),
version: process.env.APP_VERSION || '1.0.0'
version: process.env.APP_VERSION || "1.0.0",
});
}
}
+14 -51
View File
@@ -1,58 +1,21 @@
import { IControllerRegistry, ControllerRegistration, WebSocketControllerRegistration } from "@/lib/controller.interface";
import { createControllerRegistry } from "@/lib/controller.utils";
// Import controllers
import { HealthController } from "@/controllers/health.controller";
import { DocumentController } from "@/controllers/document.controller";
import { CollaborationController } from "@/controllers/collaboration.controller";
/**
* Controller Registry Module
* Centralized place to register all controllers and their dependencies
* Controller registry exports
* Simple grouped arrays of controller classes for better organization
*/
class ControllerRegistryModule {
// Define controller groups for better organization using registration format
private readonly CONTROLLERS = {
// Core system controllers (health checks, status endpoints)
CORE: [
{ Controller: HealthController }
],
// Document management controllers
DOCUMENT: [
{ Controller: DocumentController }
],
// WebSocket controllers for real-time functionality
WEBSOCKET: [
{ Controller: CollaborationController, dependencies: ['hocuspocus'] }
],
};
/**
* Get all REST controllers
*/
getAllRestControllers(): ControllerRegistration[] {
return [...this.CONTROLLERS.CORE, ...this.CONTROLLERS.DOCUMENT];
}
/**
* Get all WebSocket controllers
*/
getAllWebSocketControllers(): WebSocketControllerRegistration[] {
return this.CONTROLLERS.WEBSOCKET;
}
/**
* Create a controller registry with all configured controllers
*/
createRegistry(): IControllerRegistry {
return createControllerRegistry(
this.getAllRestControllers(),
this.getAllWebSocketControllers()
);
}
}
export const CONTROLLERS = {
// Core system controllers (health checks, status endpoints)
CORE: [HealthController],
// Export a singleton instance
export const controllerRegistry = new ControllerRegistryModule();
// Document management controllers
DOCUMENT: [DocumentController],
// WebSocket controllers for real-time functionality
WEBSOCKET: [CollaborationController],
};
// Helper to get all REST controllers
export const getAllControllers = () => [...CONTROLLERS.CORE, ...CONTROLLERS.DOCUMENT, ...CONTROLLERS.WEBSOCKET];
@@ -1,12 +1,10 @@
import { ErrorRequestHandler, Request, Response, NextFunction } from "express";
import { SentryInstance, captureException } from "@/sentry-config";
import { env } from "@/env";
import { logger } from "@plane/logger";
import { handleError } from "./error-factory";
import { ErrorContext, reportError } from "./error-reporting";
import { manualLogger } from "../logger";
import { APIService } from "@/core/services/api.service";
/**
* HTTP Status Codes
@@ -150,18 +148,6 @@ export class AppError extends Error {
context: this.context,
});
}
// Send to Sentry if configured
if (SentryInstance) {
captureException(this, {
extra: {
...this.context,
isOperational: this.isOperational,
errorCategory: this.category,
status: this.status,
},
});
}
}
}
@@ -328,10 +314,6 @@ export const setupGlobalErrorHandlers = (gracefulTerminationHandler: () => Promi
process.on("unhandledRejection", (reason: unknown) => {
logger.error("Unhandled Promise Rejection", { reason });
if (SentryInstance) {
SentryInstance.captureException(reason);
}
// Convert to AppError and handle
const appError = handleError(reason, {
errorType: "internal",
@@ -352,10 +334,6 @@ export const setupGlobalErrorHandlers = (gracefulTerminationHandler: () => Promi
stack: error.stack,
});
if (SentryInstance) {
SentryInstance.captureException(error);
}
// Convert to AppError if needed
const appError = handleError(error, {
errorType: "internal",
@@ -1,4 +1,3 @@
import { SentryInstance } from "@/sentry-config";
import { AppError, ErrorCategory } from "./error-handler";
import { logger } from "@plane/logger";
import { handleError } from "./error-factory";
@@ -27,16 +26,6 @@ export const reportError = (error: Error | unknown, context?: ErrorContext): voi
error,
context,
});
// Send to Sentry
if (SentryInstance) {
SentryInstance.captureException(error, {
extra: {
...context,
errorCategory: ErrorCategory.PROGRAMMING,
},
});
}
};
export const handleFatalError = (error: Error | unknown, context?: ErrorContext): void => {
+1 -11
View File
@@ -12,16 +12,7 @@ const envSchema = z.object({
LIVE_BASE_PATH: z.string().default("/live"),
// CORS configuration
CORS_ALLOWED_ORIGINS: z
.string()
.default("*"),
// Sentry configuration
LIVE_SENTRY_DSN: z.string().optional(),
LIVE_SENTRY_ENVIRONMENT: z.string().default("development"),
LIVE_SENTRY_TRACES_SAMPLE_RATE: z.string().default("0.5").transform(Number),
LIVE_SENTRY_RELEASE: z.string().optional(),
LIVE_SENTRY_RELEASE_VERSION: z.string().optional(),
CORS_ALLOWED_ORIGINS: z.string().default("*"),
// Compression options
COMPRESSION_LEVEL: z.string().default("6").transform(Number),
COMPRESSION_THRESHOLD: z.string().default("5000").transform(Number),
@@ -49,4 +40,3 @@ function validateEnv() {
// Export the validated environment
export const env = validateEnv();
+23 -43
View File
@@ -1,52 +1,32 @@
import { Router } from "express";
import { IControllerRegistry, IServiceContainer, ControllerRegistration, WebSocketControllerRegistration } from "./controller.interface";
import { registerControllers as registerRestControllers, registerWebSocketControllers } from "@plane/decorators";
import "reflect-metadata";
// Define valid HTTP methods
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'all' | 'use' | 'options' | 'head' | 'ws';
/**
* Register all controllers from a registry with dependency injection
* Register all controllers from the controllers array
* @param router Express router to register routes on
* @param registry Controller registry containing all controllers
* @param container Service container for dependency injection
* @param controllers Array of controller classes to register
* @param dependencies Array of dependencies to pass to controllers
*/
export function registerControllers(
router: Router,
registry: IControllerRegistry,
container: IServiceContainer
): void {
// Register REST controllers
registry.controllers.forEach((registration) => {
const { Controller, dependencies = [] } = registration;
const resolvedDependencies = dependencies.map(dep => container.get(dep));
const instance = new Controller(...resolvedDependencies);
instance.registerRoutes(router);
});
export function registerControllers(router: Router, controllers: any[], dependencies: any[] = []): void {
controllers.forEach((Controller) => {
// Create the controller instance with dependencies
const instance = new Controller(...dependencies);
// Register WebSocket controllers
registry.webSocketControllers.forEach((registration) => {
const { Controller, dependencies = [] } = registration;
const resolvedDependencies = dependencies.map(dep => container.get(dep));
const instance = new Controller(...resolvedDependencies);
// Call the specialized WebSocket registration method
instance.registerWebSocketRoutes(router);
// Determine if it's a WebSocket controller or REST controller by checking
// if it has any methods with the "ws" method metadata
const isWebsocket = Object.getOwnPropertyNames(Controller.prototype).some((methodName) => {
if (methodName === "constructor") return false;
return Reflect.getMetadata("method", instance, methodName) === "ws";
});
if (isWebsocket) {
// Register as WebSocket controller
// Pass the existing instance with dependencies to avoid creating a new instance without them
registerWebSocketControllers(router, Controller, instance);
} else {
// Register as REST controller - doesn't accept an instance parameter
registerRestControllers(router, Controller);
}
});
}
/**
* Create a controller registry with the given controllers
* @param controllers Array of REST controller registrations
* @param webSocketControllers Array of WebSocket controller registrations
* @returns Controller registry
*/
export function createControllerRegistry(
controllers: ControllerRegistration[],
webSocketControllers: WebSocketControllerRegistration[] = []
): IControllerRegistry {
return {
controllers,
webSocketControllers,
};
}
+6 -18
View File
@@ -1,19 +1,6 @@
import "reflect-metadata";
import type { RequestHandler } from "express";
import { asyncHandler } from "@/core/helpers/error-handling/error-handler";
// Re-export all decorators from our new package
export {
Controller,
Get,
Post,
Put,
Patch,
Delete,
Middleware,
WebSocket
} from "@plane/decorators";
/**
* Decorator to wrap controller methods with error handling
* This automatically catches and processes all errors using our error handling system
@@ -21,14 +8,15 @@ export {
export const CatchErrors = (): MethodDecorator => {
return function (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
// Only apply to methods that are not WebSocket handlers
const isWebSocketHandler = Reflect.getMetadata("method", target, propertyKey) === "ws";
if (typeof originalMethod === 'function' && !isWebSocketHandler) {
if (typeof originalMethod === "function" && !isWebSocketHandler) {
descriptor.value = asyncHandler(originalMethod);
}
return descriptor;
};
};
};