Skip to content

Instantly share code, notes, and snippets.

@gugadev
Created July 15, 2023 21:54
Show Gist options
  • Save gugadev/97cc3304ba3cd5984db13d8c4246dced to your computer and use it in GitHub Desktop.
Save gugadev/97cc3304ba3cd5984db13d8c4246dced to your computer and use it in GitHub Desktop.

Revisions

  1. Gustavo García created this gist Jul 15, 2023.
    213 changes: 213 additions & 0 deletions http.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,213 @@
    /**
    * @description Constituye un catálogo de errores de una API.
    * Un catálogo es un mapa en donde se asocia un código de error
    * con un mensaje describiendo el problema que se ha originado.
    * Por ejemplo:
    * {
    * 24323: 'Ha ocurrido un error consumiendo el servicio externo ABC',
    * 13424: 'No se encontraron coincidencias para la búsqueda'
    * }
    */
    export class ErrorCatalog {
    constructor(public catalog: Record<number, string>) {}

    getErrorMessage(errorCode: number): string {
    return this.catalog[errorCode];
    }
    }

    /**
    * @description Este tipo solamente es utilizado para tipificar la respuesta
    * del servicio cuando esta no es exitosa (diferente a 200).
    * Está implícito que la respuesta del servidor debe retornar un
    * campo 'code', el cual represente al código de error del catálogo.
    */
    interface ResponseError {
    code: number;
    }

    /**
    * @description Representa la respuesta que se va a transmitir al
    * repositorio. Esta puede contener una respuesta o un error.
    * @field error: representa una instancia de UnknownRequestError,
    * BadRequestError, NotFoundError o InternalServerError.
    */
    class RemoteResponse<T> {
    constructor(public status: number, public body?: T, public error?: Error) {}
    }

    /**
    * @description Representa un error en la respuesta del servidor.
    * Este error es usado en RemoteResponse para informar del error
    * a capas exteriores.
    */
    class RemoteResponseError<T> extends Error {
    constructor(
    // public message: string,
    public statusCode: number,
    public errorCode: number,
    public response: T | undefined
    ) {
    super("");
    }
    }

    /**
    * @description representa un error desconocido durante una
    * petición, es decir, cuando el código HTTP es disinto
    * a 400, 404 o 500.
    */
    export class UnknownRequestError extends Error {
    constructor(stackTrace?: string) {
    super("Error desconocido");
    this.stack = stackTrace;
    }
    }

    /**
    * @description representa un error HTTP 400.
    */
    export class BadRequestError extends Error {
    constructor(message?: string, stackTrace?: string) {
    super(message ?? "Petición inválida o mal formada");
    this.stack = stackTrace;
    }
    }

    /**
    * @description representa un error HTTP 404.
    */
    export class NotFoundError extends Error {
    constructor(stackTrace?: string) {
    super("El recurso no fue encontrado");
    this.stack = stackTrace;
    }
    }

    /**
    * @description representa un error HTTP 500.
    */
    export class InternalServerError extends Error {
    constructor(message?: string, stackTrace?: string) {
    super(message ?? "Ocurrió un error en el servidor");
    this.stack = stackTrace;
    }
    }

    /**
    * @description se utiliza cuando se espera una respuesta
    * del servicio pero no se obtiene nada.
    */
    export class NoResponseError extends Error {
    constructor() {
    super("No se obtuvo una respuesta del servidor");
    }
    }

    export type RemoteConsumerRequestProps = Omit<
    RequestInit,
    "headers" | "body"
    > & {
    headers?: Record<string, unknown>;
    body?: Record<string, unknown>;
    method?: "get" | "post" | "put" | "patch" | "delete";
    catalog?: ErrorCatalog;
    };

    export interface RemoteConsumer {
    request<T>(
    url: string,
    props?: RemoteConsumerRequestProps
    ): Promise<RemoteResponse<T | undefined>>;
    }

    export class RestConsumer implements RemoteConsumer {
    async request<T>(
    endpoint: string,
    {
    headers,
    catalog,
    body = {},
    method = "get",
    }: RemoteConsumerRequestProps = {}
    ): Promise<RemoteResponse<T | undefined>> {
    try {
    const response = await fetch(endpoint, {
    method,
    headers: headers as unknown as HeadersInit | undefined,
    ...(method === "get"
    ? {}
    : { body: body as unknown as BodyInit | undefined }),
    });

    const contentType = response.headers.get("Content-Type");
    const errorResponseCodes = [400, 404, 500];
    let data: unknown;

    if (contentType?.toLowerCase().includes("application/json")) {
    data = await response.json();
    } else if (contentType?.toLowerCase().includes("arraybuffer")) {
    data = await response.arrayBuffer();
    } else {
    data = await response.text();
    }

    if (errorResponseCodes.includes(response.status)) {
    throw new RemoteResponseError<T>(
    //"Ocurrió un error en la respuesta del servicio",
    response.status,
    (data as ResponseError).code,
    data as T
    );
    }

    return new RemoteResponse(200, data as T);
    } catch (e) {
    if (e instanceof RemoteResponseError) {
    const error = e as RemoteResponseError<T>;
    let customError: Error;
    let errorMessage: string | undefined;

    if (catalog && error.response) {
    errorMessage = catalog.getErrorMessage(error.statusCode);
    }

    switch (error.errorCode) {
    case 400: {
    customError = new BadRequestError(errorMessage, error.stack);
    break;
    }
    case 404: {
    customError = new NotFoundError(error.stack);
    break;
    }
    case 500: {
    customError = new InternalServerError(errorMessage, error.stack);
    break;
    }
    }

    return new RemoteResponse(
    error.statusCode,
    error.response,
    customError!
    );
    }
    return new RemoteResponse(
    0,
    (e as Error).message as T,
    new UnknownRequestError((e as Error).stack)
    );
    }
    }
    }

    // TODO: implementar
    class GraphQLConsumer implements RemoteConsumer {
    request<T>(
    url: string,
    props?: RemoteConsumerRequestProps
    ): Promise<RemoteResponse<T | undefined>> {
    throw new Error("Method not implemented.");
    }
    }