Created
July 15, 2023 21:54
-
-
Save gugadev/97cc3304ba3cd5984db13d8c4246dced to your computer and use it in GitHub Desktop.
Small utility to remote data fetching using polymorfism to allow distinct implementations.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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."); | |
} | |
} |
Finally you can use it with you framework of preference.
React
export function useGetUsers(useCase: IGetAllUsersUseCase) {
return useQuery({
queryKey: ["getAllUsers"],
queryFn: () => useCase.execute(),
});
}
function App() {
// better: use DI
// const { ... } = useGetAllUsers(di.resolve(GetAllUsersUseCase))
const {
data: users,
error,
isLoading,
} = useGetUsers(
new GetAllUsersUseCase(
new UsersRepository(
new RestConsumer(),
new ErrorCatalog({
1001: "Error de prueba",
1002: "Otro error de prueba",
})
)
)
);
const raiseError(message: string) {
// raise a modal or something
}
useEffect(() => {
if (error) {
raiseModal(error.message)
}
}, [error]);
return (
<div className="App">
{!users && isLoading && <h1>Cargando...</h1>}
{users && (
<table>
<thead>
<tr>
<th>Nombre</th>
<th>Email</th>
<th>Compañía</th>
<th>Sitio web</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.username}>
<td>{user.name}</td>
<td>{user.username}</td>
<td>{user.company}</td>
<td>{user.website}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
}
Angular
class UsersComponent implements OnInit {
users: Array<User> = []
constructor(private useCase: IGetAllUsersUseCase) {}
private raiseError(message: string) {
// raise modal or something
}
ngOnInit(): void {
this.useCase.execute().subscribe({
next(users) {
this.users = users
},
error(error) {
this.raiseError(error.message)
}
})
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
You should use as follows:
Entity
Repository
Use cases