Skip to content

Commit

Permalink
Merge pull request #159 from codex-team/feat/domain-error-handler
Browse files Browse the repository at this point in the history
Feat: domain error handler
  • Loading branch information
elizachi authored Jan 19, 2024
2 parents 317c682 + 62aba5a commit 3565d37
Show file tree
Hide file tree
Showing 10 changed files with 108 additions and 15 deletions.
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 — manually thrown exception provided by business logic
*/
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
17 changes: 14 additions & 3 deletions src/domain/service/noteSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Team, TeamMember, TeamMemberCreationAttributes } from '@domain/ent
import { MemberRole } from '@domain/entities/team.js';
import type User from '@domain/entities/user.js';
import { createInvitationHash } from '@infrastructure/utils/invitationHash.js';
import { DomainError } from '@domain/entities/DomainError';

/**
* Service responsible for Note Settings
Expand Down Expand Up @@ -44,7 +45,7 @@ export default class NoteSettingsService {
* Check if invitation hash is valid
*/
if (noteSettings === null) {
throw new Error(`Wrong invitation`);
throw new DomainError(`Wrong invitation`);
}

/**
Expand All @@ -53,7 +54,7 @@ export default class NoteSettingsService {
const isUserTeamMember = await this.teamRepository.isUserInTeam(userId, noteSettings.noteId);

if (isUserTeamMember) {
throw new Error(`User already in team`);
throw new DomainError(`User already in team`);
}

return await this.teamRepository.createTeamMembership({
Expand All @@ -69,7 +70,13 @@ export default class NoteSettingsService {
* @param id - note internal id
*/
public async getNoteSettingsByNoteId(id: NoteInternalId): Promise<NoteSettings> {
return await this.noteSettingsRepository.getNoteSettingsByNoteId(id);
const settings = await this.noteSettingsRepository.getNoteSettingsByNoteId(id);

if (settings === null) {
throw new DomainError(`Note settings not found`);
}

return settings;
}

/**
Expand Down Expand Up @@ -97,6 +104,10 @@ export default class NoteSettingsService {
public async patchNoteSettingsByNoteId(noteId: NoteInternalId, data: Partial<NoteSettings>): Promise<NoteSettings | null> {
const noteSettings = await this.noteSettingsRepository.getNoteSettingsByNoteId(noteId);

if (noteSettings === null) {
throw new DomainError(`Note settings not found`);
}

return await this.noteSettingsRepository.patchNoteSettingsById(noteSettings.id, data);
}

Expand Down
3 changes: 2 additions & 1 deletion src/domain/service/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Provider } from '@repository/user.repository.js';
import type User from '@domain/entities/user.js';
import type EditorTool from '@domain/entities/editorTools';
import type { SharedDomainMethods } from './shared/index.js';
import { DomainError } from '@domain/entities/DomainError.js';

export {
Provider
Expand Down Expand Up @@ -57,7 +58,7 @@ export default class UserService {
const user = await this.getUserById(userId);

if (user === null) {
throw new Error('User not found');
throw new DomainError('User not found');
}

const userToolsIds = user.editorTools ?? [];
Expand Down
30 changes: 30 additions & 0 deletions src/presentation/http/decorators/domainError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { StatusCodes } from 'http-status-codes';
import type { FastifyReply } from 'fastify';

/**
* Custom method for replying with information that business logic dismissed the request for some reason
*
* 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
*/
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 replying with information that business logic dismissed the request for some reason
*
* 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);
}
});
}
}

2 changes: 1 addition & 1 deletion src/repository/noteSettings.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default class NoteSettingsRepository {
* @param id - note id
* @returns found note settings
*/
public async getNoteSettingsByNoteId(id: NoteInternalId): Promise<NoteSettings> {
public async getNoteSettingsByNoteId(id: NoteInternalId): Promise<NoteSettings | null> {
return await this.storage.getNoteSettingsByNoteId(id);
}

Expand Down
7 changes: 2 additions & 5 deletions src/repository/storage/postgres/orm/sequelize/noteSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,18 +147,15 @@ export default class NoteSettingsSequelizeStorage {
* @param noteId - note id
* @returns { Promise<NoteSettings | null> } - note settings
*/
public async getNoteSettingsByNoteId(noteId: NoteSettings['noteId']): Promise<NoteSettings> {
public async getNoteSettingsByNoteId(noteId: NoteSettings['noteId']): Promise<NoteSettings | null> {
const settings = await this.model.findOne({
where: {
noteId: noteId,
},
});

if (!settings) {
/**
* TODO: improve exceptions
*/
throw new Error('Note settings not found');
return null;
}

return settings;
Expand Down

0 comments on commit 3565d37

Please sign in to comment.