Skip to content

Commit

Permalink
Merge pull request #49 from zulfikarrosadi/fix/error-handling
Browse files Browse the repository at this point in the history
Fix/error handling
  • Loading branch information
zulfikarrosadi authored Nov 21, 2024
2 parents 50f86ce + bb63986 commit 278960f
Show file tree
Hide file tree
Showing 12 changed files with 595 additions and 28 deletions.
433 changes: 433 additions & 0 deletions openapi.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"test": "npx jest",
"postinstall": "prisma generate",
"build": "npx prisma generate && npx tsc",
"dev": "npx ts-node-dev --respawn --transpile-only src/index.ts",
"dev": "npx ts-node-dev --respawn --rs --transpile-only src/index.ts",
"lint": "npx biome lint ./src",
"fix-lint": "npx biome lint --write --unsafe ./src",
"format": "npx biome format --write ./src",
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import cors from 'cors'
import swaggerUI from 'swagger-ui-express'
import sanitizeInput from './middlewares/sanitizeInput'
const app = express()
import swaggerDocument from '../openapi.json'
import swaggerDocument from './openapi.json'
import routes from './routes'

const port = process.env.SERVER_PORT
Expand Down
12 changes: 12 additions & 0 deletions src/lib/Error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,15 @@ export class ServerError extends Error {
this.code = 500
}
}

/**
* this is needed for image error validation and for create product price error validation
* we receive multipart/form-data and every input is a string
*/
export class CustomValidationError extends Error {
public fieldname: string
constructor(message: string, fieldname: string) {
super(message)
this.fieldname = fieldname
}
}
5 changes: 4 additions & 1 deletion src/lib/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Request } from 'express'
import multer from 'multer'
import { CloudinaryStorage } from 'multer-storage-cloudinary'
import 'dotenv/config'
import { CustomValidationError } from './Error'

cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME as string,
Expand All @@ -23,10 +24,12 @@ const fileFilter = (_: Request, file: Express.Multer.File, callback: any) => {
if (validFormat.includes(fileFormat) && mimetype.includes('image')) {
return callback(null, true)
}
console.log(file)

return callback(
new Error(
new CustomValidationError(
'invalid file format, please only upload file with .png or .jpg extension',
file.fieldname,
),
false,
)
Expand Down
5 changes: 4 additions & 1 deletion src/middlewares/formDataParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ export default function formDataParse(multer: any) {
status: 'fail',
errors: {
code: 400,
message: err.message,
message: 'validation errors',
details: {
[err.fieldname]: err.message,
},
},
})
}
Expand Down
1 change: 1 addition & 0 deletions src/middlewares/validateInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function validateInput(schema: AnyZodObject) {
schema.parse(req.body)
return next()
} catch (error: any) {
console.log(error)
return res.status(400).send({
status: 'fail',
errors: {
Expand Down
59 changes: 58 additions & 1 deletion openapi.json → src/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,64 @@
"schema": {
"$ref": "#/components/schemas/SuccessResponse"
},
"example": null
"example": {
"status": "success",
"data": {
"products": {
"id": 1,
"name": "updated kue bolu",
"description": "updated kue bolu description",
"price": 15000,
"images": ["new/image/path.png"]
}
}
}
}
}
},
"400": {
"description": "fail to update, validaiton error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"examples": {
"image error": {
"value": {
"status": "fail",
"errors": {
"code": 400,
"message": "each product can only have 5 images at max"
}
}
},
"validation errors": {
"value": {
"status": "fail",
"errors": {
"code": 400,
"message": "validation errors",
"details": {
"price": "price should be a number",
"name": "name is required"
}
}
}
},
"image not supported": {
"value": {
"status": "fail",
"errors": {
"code": 400,
"message": "validation errors",
"details": {
"images": "invalid file format, please only upload file with .png or .jpg extension"
}
}
}
}
}
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/product/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface ProductService {
id: string,
data: UpdateProductDataService,
): Promise<ApiResponse<Product>>
deleteProductById(id: string): Promise<ApiResponse<number>>
}

class ProductHandler {
Expand Down Expand Up @@ -82,6 +83,14 @@ class ProductHandler {
}
return res.status(200).json(result)
}

deleteProductById = async (req: Request<{ id: string }>, res: Response) => {
const result = await this.service.deleteProductById(req.params.id)
if (result.status === 'fail') {
return res.status(404).json(result)
}
return res.sendStatus(204)
}
}

export default ProductHandler
27 changes: 10 additions & 17 deletions src/product/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod'
import { ZodError, z } from 'zod'

export const createProduct = z.object({
name: z
Expand All @@ -7,33 +7,26 @@ export const createProduct = z.object({
description: z
.string({ message: 'product description is required' })
.min(1, 'product description is required'),
price: z.string().transform((val) => {
const valInNumber = Number.parseFloat(val)
if (Number.isNaN(valInNumber)) {
throw new Error('product price is invalid')
}
return valInNumber
}),
price: z.string(),
images: z.string().array().optional(),
})

export const updateProduct = z.object({
name: z.string({ required_error: 'name is required' }),
description: z.string({ required_error: 'description is required' }),
price: z.string({ required_error: 'price is required' }).transform((val) => {
const valInNumber = Number.parseFloat(val)
if (Number.isNaN(valInNumber)) {
throw new Error('product price is invalid')
}
return valInNumber
}),
price: z.string({ required_error: 'price is required' }),
images: z.object({
removed: z.array(z.string(), { required_error: 'this field is required' }),
}),
})

export type CreateProduct = z.TypeOf<typeof createProduct>
export type Product = z.TypeOf<typeof createProduct> & { id: bigint }
export type CreateProduct = Omit<z.TypeOf<typeof createProduct>, 'price'> & {
price: number
}
export type Product = Omit<z.TypeOf<typeof createProduct>, 'price'> & {
id: bigint
price: number
}
export type UpdateProduct = z.TypeOf<typeof updateProduct>
export type UpdateProductDataService = {
name: string
Expand Down
67 changes: 61 additions & 6 deletions src/product/service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { JsonValue } from '@prisma/client/runtime/library'
import { NotFoundError } from '../lib/Error'
import {
BadRequestError,
CustomValidationError,
NotFoundError,
} from '../lib/Error'
import { type Logger, getContext } from '../lib/logger'
import type ApiResponse from '../schema'
import type {
Expand Down Expand Up @@ -28,7 +32,7 @@ interface ProductRepository {
id: bigint
}[]
>
deleteProductById(id: number): Promise<{ affectedRows: number }>
deleteProductById(id: bigint): Promise<{ affectedRows: number }>
updateProductById(
id: bigint,
data: FlattenUpdateProduct,
Expand All @@ -45,7 +49,29 @@ class ProductService {
data: CreateProduct,
): Promise<ApiResponse<Product>> => {
try {
const newProduct = await this.repo.createProduct(data)
let priceInFloat: undefined | number
if (typeof data.price === 'string') {
priceInFloat = Number.parseFloat(data.price)
} else {
priceInFloat = data.price
}
if (Number.isNaN(priceInFloat) || !priceInFloat) {
const context = getContext()
this.logger(
'error',
'price parsing faiil',
'service',
'createProduct',
context,
)
throw new BadRequestError(
'fail to create product, make sure to insert correct information and try again',
)
}
const newProduct = await this.repo.createProduct({
...data,
price: priceInFloat,
})
const result = await this.repo.getProductById(newProduct.id)

return {
Expand Down Expand Up @@ -114,8 +140,8 @@ class ProductService {
}

deleteProductById = async (id: string): Promise<ApiResponse<number>> => {
const parsedId = Number.parseInt(id, 10)
try {
const parsedId = BigInt(id)
if (Number.isNaN(parsedId)) {
throw new NotFoundError(
"you are trying to delete the product that does'nt exists",
Expand All @@ -141,7 +167,7 @@ class ProductService {
status: 'fail',
errors: {
code: 404,
message: error.message || error,
message: 'fail to delete product, product not found',
},
}
}
Expand All @@ -158,6 +184,23 @@ class ProductService {
"you are trying to update the product that doesn't exists",
)
}
let priceInFloat: undefined | number
if (typeof data.price === 'string') {
priceInFloat = Number.parseFloat(data.price)
} else {
priceInFloat = data.price
}
if (Number.isNaN(priceInFloat) || !priceInFloat) {
const context = getContext()
this.logger(
'error',
'price parsing faiil',
'service',
'createProduct',
context,
)
throw new CustomValidationError('price should be number', 'price')
}

const oldProduct = await this.repo.getProductById(parsedId)
const allImages = [
Expand All @@ -172,7 +215,7 @@ class ProductService {
const updatedProduct = await this.repo.updateProductById(parsedId, {
name: data.name,
description: data.description,
price: data.price,
price: priceInFloat,
images: finalImages,
})

Expand All @@ -197,6 +240,18 @@ class ProductService {
'updateProductById',
context,
)
if (error instanceof CustomValidationError) {
return {
status: 'fail',
errors: {
code: 400,
message: 'validation errors',
details: {
[error.fieldname]: error.message,
},
},
}
}
if (error instanceof SyntaxError) {
return {
status: 'fail',
Expand Down
1 change: 1 addition & 0 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ router.put(
validateInput(updateProduct),
productHandler.updateProductById,
)
router.delete('/products/:id', productHandler.deleteProductById)
router.post('/products/:id/payments', paymentHandler.requestOrderToken)
router.get('/products/payments/:orderId', paymentHandler.checkOrderStatus)

Expand Down

0 comments on commit 278960f

Please # to comment.