fix: add error logging to empty catch blocks
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
# Server Duplication — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans.
|
||||
|
||||
**Goal:** Устранить дублирование в серверных роутах: asyncHandler декоратор, validateGalleryImages, findUserOrder.
|
||||
|
||||
**Architecture:** Вынос повторяющихся паттернов в shared-хелперы (`server/src/lib/`), замена ручного try/catch на декоратор.
|
||||
|
||||
**Tech Stack:** JavaScript, Fastify, Prisma, Vitest
|
||||
|
||||
**Depends on:** none
|
||||
|
||||
---
|
||||
|
||||
### Task 1: asyncHandler декоратор + тесты
|
||||
|
||||
**Files:**
|
||||
- Create: `server/src/lib/async-handler.js`
|
||||
- Test: `server/src/lib/__tests__/async-handler.test.js`
|
||||
|
||||
- [ ] **Step 1: Write failing tests**
|
||||
|
||||
```js
|
||||
// server/src/lib/__tests__/async-handler.test.js
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { asyncHandler } from '../async-handler.js'
|
||||
|
||||
describe('asyncHandler', () => {
|
||||
it('calls the handler and returns result on success', async () => {
|
||||
const handler = vi.fn().mockResolvedValue({ hello: 'world' })
|
||||
const request = {}
|
||||
const reply = { code: vi.fn().mockReturnThis(), send: vi.fn() }
|
||||
await asyncHandler(handler)(request, reply)
|
||||
expect(handler).toHaveBeenCalledWith(request, reply)
|
||||
expect(reply.send).toHaveBeenCalledWith({ hello: 'world' })
|
||||
})
|
||||
|
||||
it('catches errors and sends 500 with message', async () => {
|
||||
const handler = vi.fn().mockRejectedValue(new Error('boom'))
|
||||
const request = { log: { error: vi.fn() } }
|
||||
const reply = { code: vi.fn().mockReturnThis(), send: vi.fn() }
|
||||
await asyncHandler(handler)(request, reply)
|
||||
expect(reply.code).toHaveBeenCalledWith(500)
|
||||
expect(reply.send).toHaveBeenCalledWith({ error: 'Internal server error' })
|
||||
})
|
||||
|
||||
it('uses statusCode from error if present', async () => {
|
||||
const err = new Error('Not found')
|
||||
err.statusCode = 404
|
||||
const handler = vi.fn().mockRejectedValue(err)
|
||||
const request = { log: { error: vi.fn() } }
|
||||
const reply = { code: vi.fn().mockReturnThis(), send: vi.fn() }
|
||||
await asyncHandler(handler)(request, reply)
|
||||
expect(reply.code).toHaveBeenCalledWith(404)
|
||||
expect(reply.send).toHaveBeenCalledWith({ error: 'Not found' })
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run to verify failure**
|
||||
|
||||
Run: `cd server && npx vitest run lib/__tests__/async-handler.test.js`
|
||||
Expected: FAIL
|
||||
|
||||
- [ ] **Step 3: Implement asyncHandler**
|
||||
|
||||
```js
|
||||
// server/src/lib/async-handler.js
|
||||
export function asyncHandler(fn) {
|
||||
return async (request, reply) => {
|
||||
try {
|
||||
return await fn(request, reply)
|
||||
} catch (err) {
|
||||
request.log.error(err)
|
||||
const statusCode = err.statusCode || 500
|
||||
const message = err.statusCode ? err.message : 'Internal server error'
|
||||
return reply.code(statusCode).send({ error: message })
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests**
|
||||
|
||||
Run: `cd server && npx vitest run lib/__tests__/async-handler.test.js`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/lib/async-handler.js server/src/lib/__tests__/async-handler.test.js
|
||||
git commit -m "feat: add asyncHandler decorator for route error handling"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Применить asyncHandler в роутах
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/routes/user-orders.js`
|
||||
- Modify: `server/src/routes/user-messages.js`
|
||||
- Modify: `server/src/routes/user-payments.js`
|
||||
- Modify: `server/src/routes/admin-gallery.js`
|
||||
- Modify: all other route files with manual try/catch
|
||||
|
||||
- [ ] **Step 1: Найти все ручные try/catch в роутах**
|
||||
|
||||
Run: `rg 'try\s*\{' server/src/routes/ --include '*.js'`
|
||||
|
||||
- [ ] **Step 2: Заменить каждый try/catch на asyncHandler**
|
||||
|
||||
Before:
|
||||
```js
|
||||
fastify.get('/orders', async (request, reply) => {
|
||||
try {
|
||||
const orders = await prisma.order.findMany(...)
|
||||
return reply.send(orders)
|
||||
} catch (err) {
|
||||
request.log.error(err)
|
||||
return reply.code(500).send({ error: 'Internal server error' })
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
After:
|
||||
```js
|
||||
import { asyncHandler } from '../../lib/async-handler.js'
|
||||
|
||||
fastify.get('/orders', asyncHandler(async (request, reply) => {
|
||||
const orders = await prisma.order.findMany(...)
|
||||
return reply.send(orders)
|
||||
}))
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run lint + tests**
|
||||
|
||||
```bash
|
||||
cd server && npm run lint && npm test
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/routes/
|
||||
git commit -m "refactor: apply asyncHandler to all route handlers"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: validateGalleryImages хелпер
|
||||
|
||||
**Files:**
|
||||
- Create: `server/src/lib/validate-gallery-images.js`
|
||||
- Test: `server/src/lib/__tests__/validate-gallery-images.test.js`
|
||||
- Modify: `server/src/routes/api/admin-products.js`
|
||||
|
||||
- [ ] **Step 1: Write tests**
|
||||
|
||||
```js
|
||||
// server/src/lib/__tests__/validate-gallery-images.test.js
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { validateGalleryImages } from '../validate-gallery-images.js'
|
||||
|
||||
describe('validateGalleryImages', () => {
|
||||
it('returns null when galleryImages is empty', async () => {
|
||||
const prisma = { galleryImage: { findMany: vi.fn() } }
|
||||
const result = await validateGalleryImages(prisma, [])
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('throws when image not found', async () => {
|
||||
const prisma = { galleryImage: { findMany: vi.fn().mockResolvedValue([]) } }
|
||||
await expect(validateGalleryImages(prisma, [1])).rejects.toThrow('not found')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Implement**
|
||||
|
||||
```js
|
||||
// server/src/lib/validate-gallery-images.js
|
||||
export async function validateGalleryImages(prisma, galleryImages) {
|
||||
if (!galleryImages || galleryImages.length === 0) return null
|
||||
|
||||
const existing = await prisma.galleryImage.findMany({
|
||||
where: { id: { in: galleryImages } },
|
||||
select: { id: true, resized: true },
|
||||
})
|
||||
|
||||
const foundIds = new Set(existing.map((img) => img.id))
|
||||
const missing = galleryImages.filter((id) => !foundIds.has(id))
|
||||
if (missing.length > 0) {
|
||||
throw Object.assign(new Error(`Gallery images not found: ${missing.join(', ')}`), { statusCode: 404 })
|
||||
}
|
||||
|
||||
const notResized = existing.filter((img) => !img.resized)
|
||||
if (notResized.length > 0) {
|
||||
throw Object.assign(new Error('Some gallery images have not been processed yet. Please try again later.'), { statusCode: 400 })
|
||||
}
|
||||
|
||||
return existing
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Заменить дублирующийся код в admin-products.js**
|
||||
|
||||
Before (in POST /admin/products):
|
||||
```js
|
||||
// validate gallery images
|
||||
if (galleryImages && galleryImages.length > 0) {
|
||||
const existingImages = await prisma.galleryImage.findMany({ ... })
|
||||
// ... duplicate validation logic
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```js
|
||||
import { validateGalleryImages } from '../../lib/validate-gallery-images.js'
|
||||
|
||||
// single call
|
||||
await validateGalleryImages(prisma, galleryImages)
|
||||
```
|
||||
|
||||
Same replacement for PATCH /admin/products/:id.
|
||||
|
||||
- [ ] **Step 4: Run tests**
|
||||
|
||||
```bash
|
||||
cd server && npm run lint && npm test
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/lib/validate-gallery-images.js server/src/lib/__tests__/validate-gallery-images.test.js server/src/routes/api/admin-products.js
|
||||
git commit -m "refactor: extract validateGalleryImages helper"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: findUserOrder хелпер
|
||||
|
||||
**Files:**
|
||||
- Create: `server/src/lib/find-user-order.js`
|
||||
- Test: `server/src/lib/__tests__/find-user-order.test.js`
|
||||
- Modify: `server/src/routes/user-orders.js`
|
||||
- Modify: `server/src/routes/user-messages.js`
|
||||
- Modify: `server/src/routes/user-payments.js`
|
||||
|
||||
- [ ] **Step 1: Write tests**
|
||||
|
||||
```js
|
||||
// server/src/lib/__tests__/find-user-order.test.js
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { findUserOrder } from '../find-user-order.js'
|
||||
|
||||
describe('findUserOrder', () => {
|
||||
it('returns order when found', async () => {
|
||||
const mockOrder = { id: '1', userId: 'user1' }
|
||||
const prisma = { order: { findFirst: vi.fn().mockResolvedValue(mockOrder) } }
|
||||
const result = await findUserOrder(prisma, '1', 'user1')
|
||||
expect(result).toEqual(mockOrder)
|
||||
expect(prisma.order.findFirst).toHaveBeenCalledWith(expect.objectContaining({
|
||||
where: { id: '1', userId: 'user1' },
|
||||
}))
|
||||
})
|
||||
|
||||
it('throws 404 when not found', async () => {
|
||||
const prisma = { order: { findFirst: vi.fn().mockResolvedValue(null) } }
|
||||
await expect(findUserOrder(prisma, '999', 'user1')).rejects.toMatchObject({ statusCode: 404 })
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Implement**
|
||||
|
||||
```js
|
||||
// server/src/lib/find-user-order.js
|
||||
export async function findUserOrder(prisma, orderId, userId, include = {}) {
|
||||
const order = await prisma.order.findFirst({
|
||||
where: { id: orderId, userId },
|
||||
include,
|
||||
})
|
||||
|
||||
if (!order) {
|
||||
throw Object.assign(new Error('Order not found'), { statusCode: 404 })
|
||||
}
|
||||
|
||||
return order
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Заменить дублирующийся код в роутах**
|
||||
|
||||
Each instance of:
|
||||
```js
|
||||
const order = await prisma.order.findFirst({ where: { id, userId } })
|
||||
if (!order) return reply.code(404).send({ error: 'Order not found' })
|
||||
```
|
||||
|
||||
Becomes:
|
||||
```js
|
||||
import { findUserOrder } from '../../lib/find-user-order.js'
|
||||
const order = await findUserOrder(prisma, id, userId)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests**
|
||||
|
||||
```bash
|
||||
cd server && npm run lint && npm test
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/lib/find-user-order.js server/src/lib/__tests__/find-user-order.test.js server/src/routes/user-{orders,messages,payments}.js
|
||||
git commit -m "refactor: extract findUserOrder helper"
|
||||
```
|
||||
Reference in New Issue
Block a user