# 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" ```