Merge branch 'refactor'

This commit is contained in:
@kirill.komarov
2026-05-13 22:07:46 +05:00
parent 3c9797af4a
commit a06f9cf2c4
85 changed files with 3762 additions and 2072 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ import {
} from '../../lib/default-category.js'
import { prisma } from '../../lib/prisma.js'
export async function registerAdminCategoryRoutes(fastify, { slugify } = {}) {
export async function registerAdminCategoryRoutes(fastify) {
fastify.get(
'/api/admin/categories',
{ preHandler: [fastify.verifyAdmin] },
@@ -27,7 +27,7 @@ export async function registerAdminCategoryRoutes(fastify, { slugify } = {}) {
reply.code(400).send({ error: 'Укажите название категории' })
return
}
const slug = String(body.slug ?? '').trim() || slugify(name) || `cat-${Date.now()}`
const slug = String(body.slug ?? '').trim() || request.server.slugify(name) || `cat-${Date.now()}`
if (isUnspecifiedCategorySlug(slug)) {
reply.code(400).send({ error: `Slug «${UNSPECIFIED_CATEGORY_SLUG}» зарезервирован` })
return
+53 -13
View File
@@ -8,19 +8,59 @@ import {
} from '../../lib/upload-limits.js'
import { persistMultipartImages } from '../../lib/upload-images.js'
export async function registerAdminProductRoutes(
fastify,
{ slugify, parseMaterialsInput, mapProductForApi } = {},
) {
const CREATE_PRODUCT_SCHEMA = {
body: {
type: 'object',
required: ['title', 'priceCents'],
properties: {
title: { type: 'string', minLength: 1 },
slug: { type: 'string' },
categoryId: { type: 'string' },
priceCents: { type: 'number', minimum: 0 },
quantity: { type: 'number', minimum: 0 },
inStock: { type: 'boolean' },
leadTimeDays: { type: 'number', minimum: 1 },
shortDescription: { type: 'string' },
description: { type: 'string' },
materials: { anyOf: [{ type: 'array', items: { type: 'string' } }, { type: 'string' }] },
imageUrl: { type: 'string' },
imageUrls: { type: 'array', items: { type: 'string' } },
published: { type: 'boolean' },
},
},
}
const PATCH_PRODUCT_SCHEMA = {
body: {
type: 'object',
properties: {
title: { type: 'string', minLength: 1 },
slug: { type: 'string' },
categoryId: { type: 'string' },
priceCents: { type: 'number', minimum: 0 },
quantity: { type: 'number', minimum: 0 },
inStock: { type: 'boolean' },
leadTimeDays: { type: 'number', minimum: 1 },
shortDescription: { type: 'string' },
description: { type: 'string' },
materials: { anyOf: [{ type: 'array', items: { type: 'string' } }, { type: 'string' }] },
imageUrl: { type: 'string' },
imageUrls: { type: 'array', items: { type: 'string' } },
published: { type: 'boolean' },
},
},
}
export async function registerAdminProductRoutes(fastify) {
fastify.get(
'/api/admin/products',
{ preHandler: [fastify.verifyAdmin] },
async () => {
async (request) => {
const items = await prisma.product.findMany({
include: { category: true, images: { orderBy: { sort: 'asc' } } },
orderBy: { updatedAt: 'desc' },
})
return items.map(mapProductForApi)
return items.map((p) => request.server.mapProductForApi(p))
},
)
@@ -52,7 +92,7 @@ export async function registerAdminProductRoutes(
fastify.post(
'/api/admin/products',
{ preHandler: [fastify.verifyAdmin] },
{ preHandler: [fastify.verifyAdmin], schema: CREATE_PRODUCT_SCHEMA },
async (request, reply) => {
const body = request.body ?? {}
const title = String(body.title ?? '').trim()
@@ -60,7 +100,7 @@ export async function registerAdminProductRoutes(
reply.code(400).send({ error: 'Укажите название' })
return
}
const slug = String(body.slug ?? '').trim() || slugify(title) || `item-${Date.now()}`
const slug = String(body.slug ?? '').trim() || request.server.slugify(title) || `item-${Date.now()}`
let categoryId = String(body.categoryId ?? '').trim()
if (!categoryId) {
categoryId = (await getOrCreateUnspecifiedCategory()).id
@@ -115,7 +155,7 @@ export async function registerAdminProductRoutes(
shortDescription: body.shortDescription ? String(body.shortDescription) : null,
description: body.description ? String(body.description) : null,
quantity,
materials: JSON.stringify(parseMaterialsInput(body.materials)),
materials: JSON.stringify(request.server.parseMaterialsInput(body.materials)),
priceCents: Math.round(priceCents),
imageUrl: body.imageUrl ? String(body.imageUrl) : null,
published: Boolean(body.published),
@@ -134,13 +174,13 @@ export async function registerAdminProductRoutes(
},
include: { category: true, images: { orderBy: { sort: 'asc' } } },
})
reply.code(201).send(mapProductForApi(product))
reply.code(201).send(request.server.mapProductForApi(product))
},
)
fastify.patch(
'/api/admin/products/:id',
{ preHandler: [fastify.verifyAdmin] },
{ preHandler: [fastify.verifyAdmin], schema: PATCH_PRODUCT_SCHEMA },
async (request, reply) => {
const { id } = request.params
const body = request.body ?? {}
@@ -182,7 +222,7 @@ export async function registerAdminProductRoutes(
data.quantity = Math.floor(n)
}
if (body.materials !== undefined) {
data.materials = JSON.stringify(parseMaterialsInput(body.materials))
data.materials = JSON.stringify(request.server.parseMaterialsInput(body.materials))
}
if (body.priceCents !== undefined) {
const p = Number(body.priceCents)
@@ -254,7 +294,7 @@ export async function registerAdminProductRoutes(
data: { ...data, images: imagesUpdate },
include: { category: true, images: { orderBy: { sort: 'asc' } } },
})
return mapProductForApi(product)
return request.server.mapProductForApi(product)
},
)
+21 -4
View File
@@ -1,5 +1,21 @@
import { prisma } from '../../lib/prisma.js'
const PUBLIC_PRODUCTS_QUERY_SCHEMA = {
querystring: {
type: 'object',
properties: {
categorySlug: { type: 'string' },
q: { type: 'string' },
availability: { type: 'string', enum: ['all', 'in_stock', 'made_to_order'] },
sort: { type: 'string', enum: ['', 'price_asc', 'price_desc'] },
page: { type: 'integer', minimum: 1 },
pageSize: { type: 'integer', minimum: 1, maximum: 100 },
priceMin: { type: 'number', minimum: 0 },
priceMax: { type: 'number', minimum: 0 },
},
},
}
const EMPTY_REVIEWS_SUMMARY = Object.freeze({
approvedReviewCount: 0,
avgRating: null,
@@ -58,12 +74,13 @@ export async function approvedReviewSummariesForProducts(productIds) {
return map
}
export async function registerPublicCatalogRoutes(fastify, { mapProductForApi } = {}) {
export async function registerPublicCatalogRoutes(fastify) {
fastify.get('/api/categories', async () => {
return prisma.category.findMany({ orderBy: { sort: 'asc' } })
})
fastify.get('/api/products', async (request, reply) => {
fastify.get('/api/products', { schema: PUBLIC_PRODUCTS_QUERY_SCHEMA }, async (request, reply) => {
const { mapProductForApi } = request.server
const { categorySlug } = request.query
const qRaw = request.query?.q
const q = typeof qRaw === 'string' ? qRaw.trim() : ''
@@ -134,7 +151,7 @@ export async function registerPublicCatalogRoutes(fastify, { mapProductForApi }
const summaries = await approvedReviewSummariesForProducts(items.map((it) => it.id))
return {
items: items.map((p) => mapProductForApi(p, summaries.get(p.id) ?? EMPTY_REVIEWS_SUMMARY)),
items: items.map((p) => request.server.mapProductForApi(p, summaries.get(p.id) ?? EMPTY_REVIEWS_SUMMARY)),
total,
page,
pageSize,
@@ -152,7 +169,7 @@ export async function registerPublicCatalogRoutes(fastify, { mapProductForApi }
return
}
const summaries = await approvedReviewSummariesForProducts([product.id])
return mapProductForApi(product, summaries.get(product.id) ?? EMPTY_REVIEWS_SUMMARY)
return request.server.mapProductForApi(product, summaries.get(product.id) ?? EMPTY_REVIEWS_SUMMARY)
})
}