Skip to content
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

Update for Compatibility with Next.js ^15 #1

Open
phricardo opened this issue Jan 6, 2025 · 2 comments
Open

Update for Compatibility with Next.js ^15 #1

phricardo opened this issue Jan 6, 2025 · 2 comments

Comments

@phricardo
Copy link
Owner

No description provided.

@phricardo
Copy link
Owner Author

The change is simple, in the controller you need to change the context in handleRequest() to { params: Promise }

However, when making this change, next js versions lower than 15 will not be able to use the latest next-app-controller version. They will be forced to continue with version 1.0.0.

@phricardo
Copy link
Owner Author

import { HttpStatus } from "./HttpStatus";
import { ControllerError } from "./errors";
import { NextRequest, NextResponse } from "next/server";
import { handleError, buildResponse } from "./controllerBuilders";

export { HttpStatus, buildResponse, ControllerError };

/**
 * Extracts the Bearer token from the Authorization header.
 * @param request The incoming NextRequest.
 * @returns The Bearer token or an empty string if not found.
 */
function getAuthorizationToken(request: NextRequest): string {
  const bearerPrefix = "Bearer ";
  const authorizationHeader = request.headers.get("Authorization") || "";
  if (!authorizationHeader.startsWith(bearerPrefix)) return "";
  return authorizationHeader.slice(bearerPrefix.length);
}

class CustomHeaders extends Headers {
  private request: NextRequest;

  constructor(request: NextRequest) {
    super(request.headers);
    this.request = request;
  }

  getBearerToken(): string {
    return getAuthorizationToken(this.request as NextRequest);
  }

  /**
   * Parses and returns cookies as a key-value object.
   * @returns An object representing the cookies.
   */
  getCookies(): Record<string, string> {
    const cookieHeader = this.get("cookie") || "";
    return Object.fromEntries(
      cookieHeader.split(";").map((cookie) => {
        const [key, value] = cookie.split("=").map((part) => part.trim());
        return [key, decodeURIComponent(value)];
      })
    );
  }
}

type ControllerContext<T> = {
  request: NextRequest;
  context?: {
    params?: Record<string, string>;
    searchParams?: Record<string, string>;
  };
  body?: T;
  headers: CustomHeaders;
};

interface HandlerResponse {
  data?: unknown;
  response: {
    httpStatus: number;
  };
}

/**
 * Wrapper function to handle API route logic with error handling and context preparation.
 * @param handler The function that handles the request logic.
 * @returns A handler for Next.js API routes.
 */
export function controller<T extends HandlerResponse | NextResponse>(
  handler: (ctx: ControllerContext<T>) => Promise<HandlerResponse | NextResponse>
) {
  /**
   * Main request handler.
   * @param request The incoming NextRequest object.
   * @param context Additional context, such as route parameters.
   * @returns A Next.js Response object.
   */
  const handleRequest = async (
    request: NextRequest,
    context: { params: Promise<any> }
  ): Promise<Response> => {
    const params = context.params ? await context.params : {};
    const searchParams = Object.fromEntries(request.nextUrl.searchParams.entries());

    const requestBody: T | undefined =
      request.method !== "GET" ? await request.json().catch(() => undefined) : undefined;

    const ctx: ControllerContext<T> = {
      request,
      context: {
        params,
        searchParams,
      },
      body: requestBody,
      headers: new CustomHeaders(request),
    };

    try {
      const result = await handler(ctx);

      if (result instanceof NextResponse) {
        return result;
      }

      return buildResponse({
        data: result.data,
        response: { httpStatus: result.response.httpStatus },
      });
    } catch (error: unknown) {
      return handleError(error);
    }
  };

  /**
   * Adds authorization logic to the request handler.
   * @param authorizationCheck A function to check the token's validity.
   * @returns A handler with authorization.
   */
  handleRequest.authorize = function (
    authorizationCheck: (token: string) => boolean | Promise<boolean>
  ) {
    return async (request: NextRequest, context: { params: Promise<any> }) => {
      try {
        const token = getAuthorizationToken(request);
        const isAuthorized = await authorizationCheck(token);

        if (!isAuthorized) {
          throw new ControllerError(
            "UNAUTHORIZED",
            "Unauthorized resource access",
            HttpStatus.UNAUTHORIZED
          );
        }

        return handleRequest(request, context);
      } catch (error: unknown) {
        return handleError(error);
      }
    };
  };

  return handleRequest;
}

Implementation suggestion for compatibility :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant