Skip to content

Commit

Permalink
chore: Improvement of error handling and logging
Browse files Browse the repository at this point in the history
  • Loading branch information
claireso committed Oct 3, 2024
1 parent 30d50fb commit 5037090
Show file tree
Hide file tree
Showing 22 changed files with 168 additions and 61 deletions.
3 changes: 3 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Public server name of your website (ie: www.myjournal.com)
SERVER_NAME="localhost"

# Level of the logger
LOG_LEVEL="info"

# Database name
POSTGRES_DB="journal"
# Database user
Expand Down
2 changes: 1 addition & 1 deletion app/(admin)/admin/photos/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ const Photos = () => {
[deletePhoto, onCloseModal]
)

if (error?.response.status === 404) {
if (error && [400, 404].includes(error.response.status)) {
navigate({ page: '1' })
return null
}
Expand Down
7 changes: 6 additions & 1 deletion app/(admin)/admin/subscriptions/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const Subscriptions = () => {

const filters = { page: (page as string) ?? '1' }

const { isFetching, isFetched, isSuccess, data } = useSubscriptions(filters)
const { isFetching, isFetched, isSuccess, data, error } = useSubscriptions(filters)

const { mutate: deleteSubscription, isPending: isDeleting } = useDeleteSubscription(filters)

Expand Down Expand Up @@ -85,6 +85,11 @@ const Subscriptions = () => {
[deleteSubscription, onCloseModal]
)

if (error && [400, 404].includes(error.response.status)) {
navigate({ page: '1' })
return null
}

return (
<>
<ListHeader>
Expand Down
7 changes: 4 additions & 3 deletions app/api/photos/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NextRequest } from 'next/server'
import { revalidateTag } from 'next/cache'
import { BadRequestError } from '@domain/errors/errors'
import { createRouteHandler, withAuth } from '@api/middlewares'
import { pool as db } from '@infrastructure/db'
import { mapPhotoToPhotoDto, PhotoUpdateDtoSchema } from '@dto'
Expand All @@ -26,7 +27,7 @@ const getPhoto = async (request: NextRequest, { params }: RequestContext) => {
const id = Number(params.id)

if (isNaN(id)) {
return Response.json({}, { status: 400 })
throw new BadRequestError('Incorrect parameter “id”', { cause: { photoId: id } })
}

const photo = await photoService.getById(id)
Expand All @@ -40,7 +41,7 @@ const editPhoto = async (request: NextRequest, { params }: RequestContext) => {
const id = Number(params.id)

if (isNaN(id)) {
return Response.json({}, { status: 400 })
throw new BadRequestError('Incorrect parameter “id”', { cause: { photoId: id } })
}

const body = await request.json()
Expand All @@ -60,7 +61,7 @@ const deletePhoto = async (request: NextRequest, { params }: RequestContext) =>
const id = Number(params.id)

if (isNaN(id)) {
return Response.json({}, { status: 400 })
throw new BadRequestError('Incorrect parameter “id”', { cause: { photoId: id } })
}

await photoService.delete(id, db)
Expand Down
13 changes: 9 additions & 4 deletions app/api/photos/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { NextRequest } from 'next/server'
import { revalidateTag } from 'next/cache'
import { differenceInMinutes } from 'date-fns'
import { BadRequestError } from '@domain/errors'
import { createRouteHandler, withAuth } from '@api/middlewares'
import { IS_NOTIFICATIONS_ENABLED, sendNotification, NOTIFICATION_NEW_PHOTO } from '@infrastructure/web-push'
import { mapPhotoToPhotoDto, mapPhotosToPhotosDto, PhotoInsertDtoSchema } from '@dto'
import { photoService, subscriptionService } from '@ioc/container'
import logger from '@infrastructure/logger'

// endpoint list photos
const getPaginatedPhotos = async (request: NextRequest) => {
Expand All @@ -14,7 +16,7 @@ const getPaginatedPhotos = async (request: NextRequest) => {
page = Number(page)

if (isNaN(page) || page < 0) {
return Response.json({}, { status: 400 })
throw new BadRequestError('Incorrect search parameter “page”', { cause: { page } })
}

const paginatedPhotos = await photoService.getPaginatedPhotos(page ?? 1)
Expand Down Expand Up @@ -47,13 +49,16 @@ const createPhoto = async (request: NextRequest) => {

if (!skipNotification) {
const subscriptions = await subscriptionService.getAll()
subscriptions.map(({ subscription, id }) =>
sendNotification(subscription, NOTIFICATION_NEW_PHOTO).catch((err: any) => {
subscriptions.map(({ subscription, id }) => {
logger.debug(`Send notification (id: ${id})`)
return sendNotification(subscription, NOTIFICATION_NEW_PHOTO).catch((err: any) => {
if (err && [410, 404].includes(err.statusCode)) {
logger.warn({ err, ctx: { notificationId: id } }, 'Can not send notification')
subscriptionService.delete(id)
}
logger.error({ err, ctx: { notificationId: id } }, 'Can not send notification')
})
)
})
}
}

Expand Down
3 changes: 2 additions & 1 deletion app/api/subscriptions/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NextRequest } from 'next/server'
import { createRouteHandler, withAuth } from '@api/middlewares'
import { BadRequestError } from '@domain/errors'
import { subscriptionService } from '@ioc/container'
import { SubscriptionInsertDtoSchema } from '@dto'

Expand All @@ -10,7 +11,7 @@ const getPaginatedSubscriptions = async (request: NextRequest) => {
page = Number(page)

if (isNaN(page) || page < 0) {
return Response.json({}, { status: 400 })
throw new BadRequestError('Incorrect parameter “page”', { cause: { page } })
}

const paginatedSubscriptions = await subscriptionService.getPaginatedSubscriptions(page ?? 1)
Expand Down
4 changes: 3 additions & 1 deletion src/application/services/media/MediaService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import uploadFile from '@utils/uploadFile'

export default class MediaService {
private repository: MediaRepository
private logger: unknown

constructor(repository: MediaRepository) {
constructor(repository: MediaRepository, logger: unknown) {
this.repository = repository
this.logger = logger
}

async create(file: File) {
Expand Down
33 changes: 16 additions & 17 deletions src/application/services/photo/PhotoService.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import escape from 'lodash/escape'
import { PhotoRepository, MediaRepository } from '@domain/repositories'
import { Pager, Photos } from '@domain/entities'
import { BadRequestError, NotFoundError } from '@domain/errors/errors'
import { PhotoRepository, MediaRepository } from '@domain/repositories'

export default class PhotoService {
private repository: PhotoRepository
private mediaRepository: MediaRepository
private logger: any

constructor(repository: PhotoRepository, mediaRepository: MediaRepository) {
constructor(repository: PhotoRepository, mediaRepository: MediaRepository, logger: any) {
this.repository = repository
this.mediaRepository = mediaRepository
this.logger = logger
}

private cleanPhotoData(data: any) {
Expand All @@ -26,19 +29,19 @@ export default class PhotoService {
// check the existence of the media before creating the photo
const media = await this.mediaRepository.getById(data.media_id)
if (media === null) {
const message = 'Media is required and must exist'
// logger.error(message)
throw new Error(message)
// return Response.json({ message }, { status: 400 })
throw new BadRequestError(`Can not create photo because media does not exist`, {
cause: data
})
}

// check if the media is already linked to a photo
const linkedPhoto = await this.repository.getByMediaId(media.id)
if (linkedPhoto !== null) {
const message = `Media ${media.id} is already linked to a photo`
// logger.error(message)
throw new Error(message)
// return Response.json({ message }, { status: 400 })
throw new BadRequestError(`Can not create photo because media is already linked to a photo`, {
cause: {
data
}
})
}

const cleanedData = this.cleanPhotoData(data)
Expand All @@ -52,6 +55,7 @@ export default class PhotoService {
async update(id: number, data: any) {
const photo = await this.getById(id)

// /!\ TODO: if the media identifier changes, add the same checks as for creation
const newPhoto = this.cleanPhotoData({
title: data.title ?? photo.title,
description: data.description ?? photo.description,
Expand All @@ -67,7 +71,7 @@ export default class PhotoService {
const photo = await this.repository.getById(id)

if (photo === null) {
throw new Error('Photo not found')
throw new NotFoundError(`Not found photo`, { cause: { photoId: id } })
}

return photo
Expand All @@ -84,11 +88,6 @@ export default class PhotoService {
async delete(id: number, db: any) {
const photo = await this.getById(id)

if (photo === null) {
throw new Error('Not found Photo')
// return Response.json({}, { status: 404 })
}

// on delete photo, delete linked media
try {
// start transaction
Expand Down Expand Up @@ -116,7 +115,7 @@ export default class PhotoService {
const totalPages = Math.ceil(count / pageSize)

if (page > totalPages) {
throw new Error('Not found')
throw new NotFoundError('Page Photo not found', { cause: { page } })
}

const offset = (page - 1) * pageSize
Expand Down
9 changes: 6 additions & 3 deletions src/application/services/subscription/SubscriptionService.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { NotFoundError } from '@domain/errors/errors'
import { Pager, Subscriptions } from '@domain/entities'
import { SubscriptionRepository } from '@domain/repositories'

export default class SubscriptionService {
private repository: SubscriptionRepository
private logger: unknown

constructor(repository: SubscriptionRepository) {
constructor(repository: SubscriptionRepository, logger: unknown) {
this.repository = repository
this.logger = logger
}

async create(data: any) {
Expand All @@ -23,7 +26,7 @@ export default class SubscriptionService {
async delete(id: number) {
const subscription = await this.getById(id)
if (subscription === null) {
throw new Error('Not found')
throw new NotFoundError(`Subscription not found`, { cause: { subscriptionId: id } })
}

return this.repository.delete(id)
Expand All @@ -36,7 +39,7 @@ export default class SubscriptionService {
const totalPages = Math.ceil(count / pageSize)

if (page < 1 || (page > 1 && page > totalPages)) {
throw new Error('Not found')
throw new NotFoundError('Page subscription not found', { cause: { page } })
}

const offset = (page - 1) * pageSize
Expand Down
4 changes: 3 additions & 1 deletion src/application/services/user/UserService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { UserRepository } from '@domain/repositories'

export default class MediaService {
private repository: UserRepository
private logger: unknown

constructor(repository: UserRepository) {
constructor(repository: UserRepository, logger: unknown) {
this.repository = repository
this.logger = logger
}

async authenticate(username: string, password: string) {
Expand Down
26 changes: 26 additions & 0 deletions src/domain/errors/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export class JournalError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options)
}
}

// HTTP 400
export class BadRequestError extends JournalError {
constructor(message: string, options?: ErrorOptions) {
super(message, options)
}
}

// HTTP 404
export class NotFoundError extends JournalError {
constructor(message: string, options?: ErrorOptions) {
super(message, options)
}
}

// HTTP 422
export class UnprocessableEntityError extends JournalError {
constructor(message: string, options?: ErrorOptions) {
super(message, options)
}
}
1 change: 1 addition & 0 deletions src/domain/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './errors'
2 changes: 1 addition & 1 deletion src/infrastructure/db/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default {
log.values = values
}

logger.info(log)
logger.debug(log)

return pool.query(text, values, callback)
},
Expand Down
38 changes: 25 additions & 13 deletions src/infrastructure/logger/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
import pino from 'pino'
import pino, { Logger } from 'pino'

const isProduction = process.env.NODE_ENV === 'production'
const isTest = process.env.NODE_ENV === 'test'

const logger = pino({
name: 'main',
nestedKey: 'payload',
level: process.env.LOG_LEVEL || 'info',
browser: {
asObject: true,
disabled: ['production'].includes(process.env.NODE_ENV) === true
disabled: isProduction
},
transport:
['production'].includes(process.env.NODE_ENV) === false
? {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname'
}
transport: !isProduction
? {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname'
}
: undefined,
enabled: ['test'].includes(process.env.NODE_ENV) === false
}
: undefined,
enabled: !isTest
})

export const createContextLogger = (name: string, context?: Logger) => {
if (!context) {
return logger.child({ name })
}
return context.child({}, { msgPrefix: name })
}

export default logger
6 changes: 5 additions & 1 deletion src/infrastructure/repositories/media/MediaRepositoryImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import * as queries from './queries'

export default class MediaRepositoryImpl implements MediaRepository {
private database: any
private logger: any

constructor(database: any) {
constructor(database: any, logger: any) {
this.database = database
this.logger = logger
}

async create(data: Pick<Media, 'type' | 'name' | 'size'>): Promise<Media> {
Expand All @@ -15,7 +17,9 @@ export default class MediaRepositoryImpl implements MediaRepository {
name,
size: { width, height }
} = data
this.logger.debug(data, 'Start to insert media')
const result = await this.database.query(queries.insertMedia(), [type, name, width, height])
this.logger.debug(result.rows[0], 'New media created')
return mapRowToMedia(result.rows[0])
}

Expand Down
Loading

0 comments on commit 5037090

Please sign in to comment.