refractor code,
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { Request, Response, NextFunction } from 'express'
|
||||
import { AppError } from './index'
|
||||
|
||||
export function errorMiddleware(err: any, _req: Request, res: Response, _next: NextFunction) {
|
||||
if (err.name === 'ZodError') {
|
||||
return res.status(400).json({ error: 'validation_error', message: 'Invalid request body', issues: err.issues, statusCode: 400 })
|
||||
}
|
||||
|
||||
if (err.code === 'P2025') {
|
||||
return res.status(404).json({ error: 'not_found', message: 'Resource not found', statusCode: 404 })
|
||||
}
|
||||
|
||||
if (err.code === 'P2002') {
|
||||
return res.status(409).json({ error: 'conflict', message: 'A resource with this value already exists', statusCode: 409 })
|
||||
}
|
||||
|
||||
if (err instanceof AppError) {
|
||||
if (err.statusCode >= 500) console.error('[API Error]', err)
|
||||
return res.status(err.statusCode).json({ error: err.error, message: err.message, statusCode: err.statusCode, ...err.data })
|
||||
}
|
||||
|
||||
const statusCode = err.statusCode ?? 500
|
||||
const message = err.message ?? 'Internal server error'
|
||||
const code = err.code ?? 'internal_error'
|
||||
|
||||
if (statusCode >= 500) console.error('[API Error]', err)
|
||||
|
||||
res.status(statusCode).json({ error: code, message, statusCode })
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
export class AppError extends Error {
|
||||
readonly statusCode: number
|
||||
readonly error: string
|
||||
readonly data?: Record<string, unknown>
|
||||
|
||||
constructor(message: string, statusCode: number, error: string, data?: Record<string, unknown>) {
|
||||
super(message)
|
||||
this.statusCode = statusCode
|
||||
this.error = error
|
||||
this.data = data
|
||||
this.name = this.constructor.name
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends AppError {
|
||||
constructor(message = 'Validation error') {
|
||||
super(message, 400, 'validation_error')
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends AppError {
|
||||
constructor(message = 'Resource not found') {
|
||||
super(message, 404, 'not_found')
|
||||
}
|
||||
}
|
||||
|
||||
export class ConflictError extends AppError {
|
||||
constructor(message = 'A resource with this value already exists') {
|
||||
super(message, 409, 'conflict')
|
||||
}
|
||||
}
|
||||
|
||||
export class ForbiddenError extends AppError {
|
||||
constructor(message = 'Forbidden') {
|
||||
super(message, 403, 'forbidden')
|
||||
}
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends AppError {
|
||||
constructor(message = 'Unauthorized') {
|
||||
super(message, 401, 'unauthorized')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Response } from 'express'
|
||||
|
||||
export function ok<T>(res: Response, data: T): void {
|
||||
res.json({ data })
|
||||
}
|
||||
|
||||
export function created<T>(res: Response, data: T): void {
|
||||
res.status(201).json({ data })
|
||||
}
|
||||
|
||||
export function noContent(res: Response): void {
|
||||
res.status(204).end()
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import multer from 'multer'
|
||||
import { ValidationError } from '../errors'
|
||||
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10 MB
|
||||
|
||||
/**
|
||||
* Shared multer instance used by all upload endpoints.
|
||||
* Files are kept in memory so services receive a Buffer — no temp-file cleanup needed.
|
||||
*/
|
||||
export const imageUpload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: MAX_FILE_SIZE },
|
||||
})
|
||||
|
||||
/**
|
||||
* Asserts that a file was provided and is an image MIME type.
|
||||
* Call inside the route handler after the multer middleware runs.
|
||||
*/
|
||||
export function assertImageFile(
|
||||
file: Express.Multer.File | undefined,
|
||||
fieldLabel = 'file',
|
||||
): asserts file is Express.Multer.File {
|
||||
if (!file) throw new ValidationError(`A ${fieldLabel} is required`)
|
||||
if (!file.mimetype.startsWith('image/')) throw new ValidationError('Only image uploads are supported')
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that at least one file was provided and all files are images.
|
||||
*/
|
||||
export function assertImageFiles(files: Express.Multer.File[], fieldLabel = 'photos'): void {
|
||||
if (!files || files.length === 0) throw new ValidationError(`At least one ${fieldLabel} file is required`)
|
||||
const nonImage = files.find((f) => !f.mimetype.startsWith('image/'))
|
||||
if (nonImage) throw new ValidationError(`All uploaded files must be images — "${nonImage.originalname}" is not`)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Request } from 'express'
|
||||
import type { ZodTypeAny, output } from 'zod'
|
||||
|
||||
export function parseBody<TSchema extends ZodTypeAny>(schema: TSchema, req: Request): output<TSchema> {
|
||||
return schema.parse(req.body)
|
||||
}
|
||||
|
||||
export function parseQuery<TSchema extends ZodTypeAny>(schema: TSchema, req: Request): output<TSchema> {
|
||||
return schema.parse(req.query)
|
||||
}
|
||||
|
||||
export function parseParams<TSchema extends ZodTypeAny>(schema: TSchema, req: Request): output<TSchema> {
|
||||
return schema.parse(req.params)
|
||||
}
|
||||
Reference in New Issue
Block a user