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

Feat: domain error handler #159

Merged
merged 9 commits into from
Jan 19, 2024
11 changes: 11 additions & 0 deletions src/domain/entities/DomainError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* DomainError entity
neSpecc marked this conversation as resolved.
Show resolved Hide resolved
*/
export class DomainError extends Error {
/**
* @param message - description of why the error was thrown
*/
constructor(message: string) {
super(message);
}
}
7 changes: 4 additions & 3 deletions src/domain/service/note.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Note, NoteInternalId, NotePublicId } from '@domain/entities/note.js';
import type NoteRepository from '@repository/note.repository.js';
import { createPublicId } from '@infrastructure/utils/id.js';
import { DomainError } from '@domain/entities/DomainError';

/**
* Note service
Expand Down Expand Up @@ -54,7 +55,7 @@ export default class NoteService {
const updatedNote = await this.repository.updateNoteContentById(id, content);

if (updatedNote === null) {
throw new Error(`Note with id ${id} was not updated`);
throw new DomainError(`Note with id ${id} was not updated`);
}

return updatedNote;
Expand All @@ -69,7 +70,7 @@ export default class NoteService {
const note = await this.repository.getNoteById(id);

if (note === null) {
throw new Error(`Note with id ${id} was not found`);
throw new DomainError(`Note with id ${id} was not found`);
}

return note;
Expand All @@ -84,7 +85,7 @@ export default class NoteService {
const note = await this.repository.getNoteByPublicId(publicId);

if (note === null) {
throw new Error(`Note with public id ${publicId} was not found`);
throw new DomainError(`Note with public id ${publicId} was not found`);
}

return note;
Expand Down
24 changes: 24 additions & 0 deletions src/presentation/http/decorators/domainError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { StatusCodes } from 'http-status-codes';
import type { FastifyReply } from 'fastify';

/**
* Custom method for sending 500 error
neSpecc marked this conversation as resolved.
Show resolved Hide resolved
*
* Send this error when a domain-level error is thrown
*
* @example
*
* if (note.creatorId !== userId) {
neSpecc marked this conversation as resolved.
Show resolved Hide resolved
* return reply.domainError('Note with id ${id} was not updated');
* }
*
* @param message - custom message
*/
export default async function domainError(this: FastifyReply, message = 'Domain level error'): Promise<void> {
await this
.code(StatusCodes.BAD_REQUEST)
.type('application/json')
.send({
message,
});
}
4 changes: 3 additions & 1 deletion src/presentation/http/decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import notFound from './notFound.js';
import forbidden from './forbidden.js';
import unauthorized from './unauthorized.js';
import notAcceptable from './notAcceptable.js';
import domainError from './domainError.js';

export {
notFound,
forbidden,
unauthorized,
notAcceptable
notAcceptable,
domainError
};
21 changes: 21 additions & 0 deletions src/presentation/http/fastify.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,26 @@ declare module 'fastify' {
* @param message - Optional message to send. If not specified, default message will be sent
*/
notAcceptable: (message?: string) => Promise<void>;

/**
* Custom method for sending 400 error
*
* Send this error when a domain-level error is thrown
*
* @example
*
* try {
* service.someAction()
* } catch (error: Error) {
* if (error instanceof DomainError) {
* reply.domainError(error.message);
* return;
* }
* throw error;
* }
*
* @param message - Optional message to send. If not specified, default message will be sent
*/
domainError: (message?: string) => Promise<void>;
}
}
21 changes: 20 additions & 1 deletion src/presentation/http/http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import fastifySwagger from '@fastify/swagger';
import fastifySwaggerUI from '@fastify/swagger-ui';
import addUserIdResolver from '@presentation/http/middlewares/common/userIdResolver.js';
import cookie from '@fastify/cookie';
import { notFound, forbidden, unauthorized, notAcceptable } from './decorators/index.js';
import { notFound, forbidden, unauthorized, notAcceptable, domainError } from './decorators/index.js';
import NoteRouter from '@presentation/http/router/note.js';
import OauthRouter from '@presentation/http/router/oauth.js';
import AuthRouter from '@presentation/http/router/auth.js';
Expand All @@ -26,6 +26,7 @@ import NoteListRouter from '@presentation/http/router/noteList.js';
import { EditorToolSchema } from './schema/EditorTool.js';
import JoinRouter from '@presentation/http/router/join.js';
import { JoinSchemaParams, JoinSchemaResponse } from './schema/Join.js';
import { DomainError } from '@domain/entities/DomainError.js';


const appServerLogger = getLogger('appServer');
Expand Down Expand Up @@ -81,6 +82,8 @@ export default class HttpApi implements Api {
this.addPoliciesCheckHook();

await this.addApiRoutes(domainServices);

this.domainErrorHandler();
}


Expand Down Expand Up @@ -287,6 +290,7 @@ export default class HttpApi implements Api {
this.server?.decorateReply('forbidden', forbidden);
this.server?.decorateReply('unauthorized', unauthorized);
this.server?.decorateReply('notAcceptable', notAcceptable);
this.server?.decorateReply('domainError', domainError);
}

/**
Expand Down Expand Up @@ -329,5 +333,20 @@ export default class HttpApi implements Api {
});
});
}

/**
* Domain error handler
*/
private domainErrorHandler(): void {
this.server?.setErrorHandler(function (error, request, reply) {
/**
* If we have an error that occurs in the domain-level we reply it with special format
*/
if (error instanceof DomainError) {
this.log.error(error);
void reply.domainError(error.message);
}
});
}
}

Loading