base commit

This commit is contained in:
@kirill.komarov
2026-04-28 22:15:12 +05:00
parent d40edf97e7
commit 3f7fdb1e15
19 changed files with 929 additions and 47 deletions
+281
View File
@@ -10,6 +10,8 @@
"dependencies": {
"@fastify/cors": "^11.2.0",
"@fastify/jwt": "^10.0.0",
"@fastify/multipart": "^10.0.0",
"@fastify/static": "^9.1.3",
"@prisma/client": "5.22.0",
"bcryptjs": "^3.0.3",
"dotenv": "^17.4.2",
@@ -20,6 +22,22 @@
"prisma": "5.22.0"
}
},
"node_modules/@fastify/accept-negotiator": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz",
"integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/@fastify/ajv-compiler": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz",
@@ -41,6 +59,12 @@
"fast-uri": "^3.0.0"
}
},
"node_modules/@fastify/busboy": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz",
"integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==",
"license": "MIT"
},
"node_modules/@fastify/cors": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.2.0.tgz",
@@ -61,6 +85,22 @@
"toad-cache": "^3.7.0"
}
},
"node_modules/@fastify/deepmerge": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-3.2.1.tgz",
"integrity": "sha512-N5Oqvltoa2r9z1tbx4xjky0oRR60v+T47Ic4J1ukoVQcptLOrIdRnCSdTGmOmajZuHVKlTnfcmrjyqsGEW1ztA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/@fastify/error": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
@@ -154,6 +194,29 @@
"dequal": "^2.0.3"
}
},
"node_modules/@fastify/multipart": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-10.0.0.tgz",
"integrity": "sha512-pUx3Z1QStY7E7kwvDTIvB6P+rF5lzP+iqPgZyJyG3yBJVPvQaZxzDHYbQD89rbY0ciXrMOyGi8ezHDVexLvJDA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"@fastify/busboy": "^3.0.0",
"@fastify/deepmerge": "^3.0.0",
"@fastify/error": "^4.0.0",
"fastify-plugin": "^5.0.0",
"secure-json-parse": "^4.0.0"
}
},
"node_modules/@fastify/proxy-addr": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz",
@@ -174,6 +237,53 @@
"ipaddr.js": "^2.1.0"
}
},
"node_modules/@fastify/send": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz",
"integrity": "sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"@lukeed/ms": "^2.0.2",
"escape-html": "~1.0.3",
"fast-decode-uri-component": "^1.0.1",
"http-errors": "^2.0.0",
"mime": "^3"
}
},
"node_modules/@fastify/static": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-9.1.3.tgz",
"integrity": "sha512-aXrYtsiryLhRxRNaxNqsn7FUISeb7rB9q4eHUPIot5aeQBLNahnz1m6thzm7JWC1poSGXS9XrX8DvuMivp2hkQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"@fastify/accept-negotiator": "^2.0.0",
"@fastify/send": "^4.0.0",
"content-disposition": "^1.0.1",
"fastify-plugin": "^5.0.0",
"fastq": "^1.17.1",
"glob": "^13.0.0"
}
},
"node_modules/@lukeed/ms": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz",
@@ -337,6 +447,15 @@
"fastq": "^1.17.1"
}
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/bcryptjs": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
@@ -352,6 +471,31 @@
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/content-disposition": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
@@ -365,6 +509,15 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -395,6 +548,12 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/fast-decode-uri-component": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
@@ -591,6 +750,43 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/glob": {
"version": "13.0.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
"license": "BlueOak-1.0.0",
"dependencies": {
"minimatch": "^10.2.2",
"minipass": "^7.1.3",
"path-scurry": "^2.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -668,12 +864,57 @@
],
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "11.3.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz",
"integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==",
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"license": "ISC"
},
"node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minipass": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mnemonist": {
"version": "0.40.3",
"resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.3.tgz",
@@ -707,6 +948,22 @@
"node": ">=14.0.0"
}
},
"node_modules/path-scurry": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/pino": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz",
@@ -921,6 +1178,12 @@
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/sonic-boom": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
@@ -939,6 +1202,15 @@
"node": ">= 10.x"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/steed": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/steed/-/steed-1.1.3.tgz",
@@ -973,6 +1245,15 @@
"node": ">=12"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+2
View File
@@ -16,6 +16,8 @@
"dependencies": {
"@fastify/cors": "^11.2.0",
"@fastify/jwt": "^10.0.0",
"@fastify/multipart": "^10.0.0",
"@fastify/static": "^9.1.3",
"@prisma/client": "5.22.0",
"bcryptjs": "^3.0.3",
"dotenv": "^17.4.2",
@@ -0,0 +1,25 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Product" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"shortDescription" TEXT,
"description" TEXT,
"priceCents" INTEGER NOT NULL,
"imageUrl" TEXT,
"published" BOOLEAN NOT NULL DEFAULT false,
"inStock" BOOLEAN NOT NULL DEFAULT true,
"leadTimeDays" INTEGER,
"categoryId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Product_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_Product" ("categoryId", "createdAt", "description", "id", "imageUrl", "priceCents", "published", "slug", "title", "updatedAt") SELECT "categoryId", "createdAt", "description", "id", "imageUrl", "priceCents", "published", "slug", "title", "updatedAt" FROM "Product";
DROP TABLE "Product";
ALTER TABLE "new_Product" RENAME TO "Product";
CREATE UNIQUE INDEX "Product_slug_key" ON "Product"("slug");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
@@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE "ProductImage" (
"id" TEXT NOT NULL PRIMARY KEY,
"url" TEXT NOT NULL,
"sort" INTEGER NOT NULL DEFAULT 0,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"productId" TEXT NOT NULL,
CONSTRAINT "ProductImage_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE INDEX "ProductImage_productId_sort_idx" ON "ProductImage"("productId", "sort");
+17
View File
@@ -20,15 +20,32 @@ model Product {
id String @id @default(cuid())
title String
slug String @unique
shortDescription String?
description String?
/// Цена в копейках (целое число, без дробной части)
priceCents Int
imageUrl String?
published Boolean @default(false)
inStock Boolean @default(true)
leadTimeDays Int?
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
categoryId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
images ProductImage[]
}
model ProductImage {
id String @id @default(cuid())
url String
sort Int @default(0)
createdAt DateTime @default(now())
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
productId String
@@index([productId, sort])
}
model User {
+16
View File
@@ -2,6 +2,9 @@ import 'dotenv/config'
import Fastify from 'fastify'
import cors from '@fastify/cors'
import jwt from '@fastify/jwt'
import multipart from '@fastify/multipart'
import fastifyStatic from '@fastify/static'
import path from 'node:path'
import { registerAuth } from './plugins/auth.js'
import { registerApiRoutes } from './routes/api.js'
import { registerAuthRoutes } from './routes/auth.js'
@@ -23,6 +26,19 @@ await fastify.register(jwt, {
secret: process.env.JWT_SECRET || 'dev-jwt-secret-change-me',
})
await fastify.register(multipart, {
limits: {
files: 10,
fileSize: 10 * 1024 * 1024,
},
})
const uploadsDir = path.join(process.cwd(), 'uploads')
await fastify.register(fastifyStatic, {
root: uploadsDir,
prefix: '/uploads/',
})
fastify.decorate('authenticate', async function authenticate(request, reply) {
try {
await request.jwtVerify()
+114 -6
View File
@@ -1,4 +1,7 @@
import { prisma } from '../lib/prisma.js'
import crypto from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
function slugify(input) {
return input
@@ -8,6 +11,12 @@ function slugify(input) {
.replace(/[^a-z0-9-а-яё]/gi, '')
}
function safeExtFromFilename(filename) {
const ext = path.extname(String(filename || '')).toLowerCase()
const allowed = new Set(['.png', '.jpg', '.jpeg', '.webp'])
return allowed.has(ext) ? ext : null
}
export async function registerApiRoutes(fastify) {
fastify.get('/api/categories', async () => {
return prisma.category.findMany({ orderBy: { sort: 'asc' } })
@@ -21,7 +30,7 @@ export async function registerApiRoutes(fastify) {
}
return prisma.product.findMany({
where,
include: { category: true },
include: { category: true, images: { orderBy: { sort: 'asc' } } },
orderBy: { createdAt: 'desc' },
})
})
@@ -30,7 +39,7 @@ export async function registerApiRoutes(fastify) {
const { id } = request.params
const product = await prisma.product.findFirst({
where: { id, published: true },
include: { category: true },
include: { category: true, images: { orderBy: { sort: 'asc' } } },
})
if (!product) {
reply.code(404).send({ error: 'Товар не найден' })
@@ -46,12 +55,45 @@ export async function registerApiRoutes(fastify) {
{ preHandler: [fastify.verifyAdmin] },
async () => {
return prisma.product.findMany({
include: { category: true },
include: { category: true, images: { orderBy: { sort: 'asc' } } },
orderBy: { updatedAt: 'desc' },
})
},
)
fastify.post(
'/api/admin/uploads',
{ preHandler: [fastify.verifyAdmin] },
async (request, reply) => {
if (!request.isMultipart()) {
reply.code(400).send({ error: 'Ожидается multipart/form-data' })
return
}
const uploadsDir = path.join(process.cwd(), 'uploads')
await fs.promises.mkdir(uploadsDir, { recursive: true })
const urls = []
const parts = request.parts()
for await (const part of parts) {
if (part.type !== 'file') continue
const ext = safeExtFromFilename(part.filename)
if (!ext) {
reply.code(400).send({ error: 'Разрешены только файлы: png, jpg, jpeg, webp' })
return
}
const id = crypto.randomUUID()
const fileName = `${id}${ext}`
const fullPath = path.join(uploadsDir, fileName)
await fs.promises.writeFile(fullPath, await part.toBuffer())
urls.push(`/uploads/${fileName}`)
}
return { urls }
},
)
fastify.post(
'/api/admin/products',
{ preHandler: [fastify.verifyAdmin] },
@@ -74,6 +116,19 @@ export async function registerApiRoutes(fastify) {
reply.code(400).send({ error: 'Некорректная цена (priceCents ≥ 0)' })
return
}
const inStock =
body.inStock === undefined || body.inStock === null ? true : Boolean(body.inStock)
const leadTimeDaysRaw = body.leadTimeDays
const leadTimeDays =
leadTimeDaysRaw === undefined || leadTimeDaysRaw === null || leadTimeDaysRaw === ''
? null
: Number(leadTimeDaysRaw)
if (!inStock) {
if (!Number.isFinite(leadTimeDays) || leadTimeDays <= 0) {
reply.code(400).send({ error: 'Если "под заказ", укажите срок исполнения (дней) > 0' })
return
}
}
const exists = await prisma.product.findUnique({ where: { slug } })
if (exists) {
reply.code(409).send({ error: 'Такой slug уже занят' })
@@ -83,13 +138,25 @@ export async function registerApiRoutes(fastify) {
data: {
title,
slug,
shortDescription: body.shortDescription ? String(body.shortDescription) : null,
description: body.description ? String(body.description) : null,
priceCents: Math.round(priceCents),
imageUrl: body.imageUrl ? String(body.imageUrl) : null,
published: Boolean(body.published),
inStock,
leadTimeDays: inStock ? null : Math.round(leadTimeDays),
categoryId,
images: Array.isArray(body.imageUrls)
? {
create: body.imageUrls
.map((u) => String(u || '').trim())
.filter(Boolean)
.slice(0, 10)
.map((u, idx) => ({ url: u, sort: idx })),
}
: undefined,
},
include: { category: true },
include: { category: true, images: { orderBy: { sort: 'asc' } } },
})
reply.code(201).send(product)
},
@@ -121,6 +188,9 @@ export async function registerApiRoutes(fastify) {
data.slug = s
}
}
if (body.shortDescription !== undefined) {
data.shortDescription = body.shortDescription ? String(body.shortDescription) : null
}
if (body.description !== undefined) {
data.description = body.description ? String(body.description) : null
}
@@ -138,10 +208,48 @@ export async function registerApiRoutes(fastify) {
if (body.published !== undefined) data.published = Boolean(body.published)
if (body.categoryId !== undefined) data.categoryId = String(body.categoryId)
if (body.inStock !== undefined) data.inStock = Boolean(body.inStock)
if (body.leadTimeDays !== undefined) {
const v = body.leadTimeDays
const n = v === null || v === '' ? null : Number(v)
if (n !== null && (!Number.isFinite(n) || n <= 0)) {
reply.code(400).send({ error: 'Срок исполнения должен быть числом дней > 0' })
return
}
data.leadTimeDays = n === null ? null : Math.round(n)
}
const nextInStock = data.inStock ?? existing.inStock
const nextLead = data.leadTimeDays ?? existing.leadTimeDays
if (!nextInStock && (!Number.isFinite(nextLead) || nextLead === null || nextLead <= 0)) {
reply.code(400).send({ error: 'Если "под заказ", укажите срок исполнения (дней) > 0' })
return
}
if (nextInStock && data.leadTimeDays !== undefined) {
data.leadTimeDays = null
}
const imagesUpdate =
body.imageUrls !== undefined
? {
deleteMany: {},
create: Array.isArray(body.imageUrls)
? body.imageUrls
.map((u) => String(u || '').trim())
.filter(Boolean)
.slice(0, 10)
.map((u, idx) => ({ url: u, sort: idx }))
: [],
}
: undefined
const product = await prisma.product.update({
where: { id },
data,
include: { category: true },
data: {
...data,
images: imagesUpdate,
},
include: { category: true, images: { orderBy: { sort: 'asc' } } },
})
return product
},
+1
View File
@@ -0,0 +1 @@