Edits history of script submission #22277 for ' Lightweight Http Router Module (http)'

  • deno
    /**
     * Supported HTTP methods for routing.
     */
    export enum METHOD {
      GET = "GET",
      POST = "POST",
      PUT = "PUT",
      PATCH = "PATCH",
      DELETE = "DELETE",
    }
    
    /**
     * The standard request object passed to route handlers.
     * Contains parsed URL components, headers, and body.
     */
    type HttpRequest = {
      /** HTTP method (e.g., "GET", "POST") */
      method: string;
      /** The matched route pattern (e.g., "/users/:id") */
      route: string;
      /** The full URL path */
      path: string;
      /** Extracted path parameters (e.g., { id: "123" }) */
      params: Record<string, string>;
      /** Query string parameters (e.g., { sort: "asc" }) */
      query: Record<string, string>;
      /** Request headers */
      headers: Record<string, string>;
      /** Parsed request body */
      body: any;
      /** Raw request string (if applicable) */
      raw_string: string | null;
    };
    
    /**
     * Windmill HTTP event object, extending HttpRequest with trigger metadata.
     */
    type HttpEvent = HttpRequest & {
      /** Always "http" for HTTP triggers */
      kind: "http";
      /** The path used to trigger the script */
      trigger_path: string;
    };
    
    /**
     * A lightweight, chainable router for Windmill HTTP scripts.
     * Supports method chaining, custom error handling, and automatic response formatting.
     */
    export class Router {
      private routes: Record<string, Record<string, (req: HttpRequest) => any>> = {};
    
      /**
       * Preprocessor hook for Windmill.
       * Validates the event kind and prepares the request object.
       * @param event - The incoming Windmill event
       * @returns An object containing the normalized request
       */
      preprocessor = async function (event: HttpEvent) {
        if (event.kind === "http") {
          return { req: event };
        }
        throw new Error(`Unsupported trigger kind: ${event.kind}`);
      };
    
      /**
       * Normalizes HTTP method strings to uppercase for consistent routing.
       */
      private normalizeMethod(method: string): string {
        return method.toUpperCase();
      }
    
      /**
       * Registers a GET route.
       * @param route - The URL path pattern (e.g., "/users")
       * @param handler - The function to execute when the route is matched
       */
      get(route: string, handler: (req: HttpRequest) => any) {
        return this.addRoute(METHOD.GET, route, handler);
      }
    
      /**
       * Registers a POST route.
       * @param route - The URL path pattern
       * @param handler - The function to execute
       */
      post(route: string, handler: (req: HttpRequest) => any) {
        return this.addRoute(METHOD.POST, route, handler);
      }
    
      /**
       * Registers a PUT route.
       * @param route - The URL path pattern
       * @param handler - The function to execute
       */
      put(route: string, handler: (req: HttpRequest) => any) {
        return this.addRoute(METHOD.PUT, route, handler);
      }
    
      /**
       * Registers a PATCH route.
       * @param route - The URL path pattern
       * @param handler - The function to execute
       */
      patch(route: string, handler: (req: HttpRequest) => any) {
        return this.addRoute(METHOD.PATCH, route, handler);
      }
    
      /**
       * Registers a DELETE route.
       * @param route - The URL path pattern
       * @param handler - The function to execute
       */
      delete(route: string, handler: (req: HttpRequest) => any) {
        return this.addRoute(METHOD.DELETE, route, handler);
      }
    
      /**
       * Internal method to register a route.
       * @param method - The HTTP method
       * @param route - The path pattern
       * @param handler - The handler function
       * @returns The Router instance for chaining
       */
      private addRoute(method: METHOD, route: string, handler: (req: HttpRequest) => any) {
        if (!this.routes[method]) {
          this.routes[method] = {};
        }
        this.routes[method][route] = handler;
        return this;
      }
    
      /**
       * Executes the matched handler or throws an appropriate HttpError.
       * @param req - The request object
       * @returns The handler's return value
       * @throws HttpError if route or method is not found
       */
      private executeHandler(req: HttpRequest): any {
        const method = this.normalizeMethod(req.method);
        const route = req.route;
    
        if (!this.routes[method]) {
          throw HttpError.MethodNotAllowed(`Method ${method} not allowed`);
        }
    
        const handler = this.routes[method][route];
    
        if (!handler) {
          throw HttpError.NotFound(`Route ${route} not found`);
        }
    
        return handler(req);
      }
    
      /**
       * Main entry point for handling requests.
       * Wraps execution in try-catch to standardize error responses.
       * @param req - The request object
       * @returns A formatted WindmillResponse object
       */
      handle(req: HttpRequest): any {
        try {
          return this.executeHandler(req);
        } catch (error) {
          console.error("Router Error:", error);
    
          if (!(error instanceof HttpError)) {
            // Wrap unexpected errors in a 500 HttpError
            throw HttpError.InternalError(undefined, error);
          }
    
          // Format HttpError for Windmill
          return {
            windmill_status_code: error.status,
            result: {
              statusCode: error.status,
              error: error.message
            }
          };
        }
      }
    }
    
    /**
     * Custom error class for HTTP status codes.
     * Extends the native Error class with a status code and optional cause.
     */
    export class HttpError extends Error {
      status: number;
    
      /**
       * Creates a new HttpError.
       * @param status - HTTP status code (e.g., 404, 500)
       * @param message - Error message
       * @param cause - The original error that caused this one (preserves stack trace)
       */
      constructor(status: number, message: string, cause?: any) {
        super(message);
        this.status = status;
        this.name = 'HttpError';
        // Manual assignment for runtime compatibility in serverless environments
        if (cause) {
          (this as any).cause = cause;
        }
      }
    
      static BadRequest(message = 'Bad Request', cause?: any) {
        return new HttpError(400, message, cause);
      }
    
      static Unauthorized(message = 'Unauthorized', cause?: any) {
        return new HttpError(401, message, cause);
      }
    
      static Forbidden(message = 'Forbidden', cause?: any) {
        return new HttpError(403, message, cause);
      }
    
      static NotFound(message = 'Not found', cause?: any) {
        return new HttpError(404, message, cause);
      }
    
      static MethodNotAllowed(message = 'Method not allowed', cause?: any) {
        return new HttpError(405, message, cause);
      }
    
      static UnprocessableContent(message = 'Unprocessable Content', cause?: any) {
        return new HttpError(422, message, cause);
      }
    
      static InternalError(message = 'Internal server error', cause?: any) {
        return new HttpError(500, message, cause);
      }
    }
    
    /**
     * Standard Windmill HTTP response structure.
     */
    type WindmillResponse = {
      windmill_status_code: number;
      windmill_headers?: Record<string, string>;
      result: any;
    };
    
    /**
     * Helper class to construct HTTP responses with convenient status codes.
     * Automatically formats output for Windmill.
     */
    export class HttpResponse {
      public status: number;
      public data: any;
      public headers?: Record<string, string>;
    
      constructor(data: any, status: number = 200, headers?: Record<string, string>) {
        this.data = data;
        this.status = status;
        this.headers = headers;
      }
    
      /**
       * Returns a 200 OK response.
       */
      static Ok(data: any, headers?: Record<string, string>) {
        return new HttpResponse(data, 200, headers).toWindmillResponse();
      }
    
      /**
       * Returns a 201 Created response.
       */
      static Created(data: any, headers?: Record<string, string>) {
        return new HttpResponse(data, 201, headers).toWindmillResponse();
      }
    
      /**
       * Returns a 202 Accepted response.
       */
      static Accepted(data: any, headers?: Record<string, string>) {
        return new HttpResponse(data, 202, headers).toWindmillResponse();
      }
    
      /**
       * Returns a 204 No Content response.
       */
      static NoContent(headers?: Record<string, string>) {
        return new HttpResponse(null, 204, headers).toWindmillResponse();
      }
    
      private toWindmillResponse(): WindmillResponse {
        return {
          windmill_status_code: this.status,
          windmill_headers: this.headers,
          result: this.data
        };
      }
    }
    
    // --- Usage Example ---
    
    const router = new Router();
    
    router
      .get("router-test", (req) => {
        return HttpResponse.Ok({ message: "GET TEST" });
      })
      .post("router-test", (req) => {
        if (true) {
          throw HttpError.Forbidden();
        }
    
        return HttpResponse.NoContent();
      });
    
    // # REQUEST MIDDLEWARE
    export async function preprocessor(event: HttpEvent) {
      return await router.preprocessor(event);
    }
    
    // # MAIN ENTRY POINT
    export async function main(req: HttpRequest) {
      return router.handle(req);
    }

    Submitted by as900 60 days ago