Магазин на JavaScript, часть 8 из 19. Работа со свойствами товара и корзиной покупателя
04.12.2021
Теги: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • Корзина • Фреймворк
Свойства товара
Сейчас работать со свойствами товара не слишком удобно. Если нам надо добавить новое свойство, отредактировать или удалить существующее, надо обновить товар целиком. Тогда все старые свойства товара будут удалены, а новые добавлены. Давайте это исправим, чтобы со свойствами товаров можно было работать точечно, с каждым по отдельности.
Добавляем новые маршруты в файл routes/product.js
:
import express from 'express' import ProductController from '../controllers/Product.js' import ProductPropController from '../controllers/ProductProp.js' import authMiddleware from '../middleware/authMiddleware.js' import adminMiddleware from '../middleware/adminMiddleware.js' const router = new express.Router() /* * Товары */ // список товаров выбранной категории и выбранного бренда router.get('/getall/categoryId/:categoryId([0-9]+)/brandId/:brandId([0-9]+)', ProductController.getAll) // список товаров выбранной категории router.get('/getall/categoryId/:categoryId([0-9]+)', ProductController.getAll) // список товаров выбранного бренда router.get('/getall/brandId/:brandId([0-9]+)', ProductController.getAll) // список всех товаров каталога router.get('/getall', ProductController.getAll) // получить один товар каталога router.get('/getone/:id([0-9]+)', ProductController.getOne) // создать товар каталога — нужны права администратора router.post('/create', authMiddleware, adminMiddleware, ProductController.create) // обновить товар каталога — нужны права администратора router.put('/update/:id([0-9]+)', authMiddleware, adminMiddleware, ProductController.update) // удалить товар каталога — нужны права администратора router.delete('/delete/:id([0-9]+)', authMiddleware, adminMiddleware, ProductController.delete) /* * Свойства */ // список свойств товара router.get('/product/:productId([0-9]+)/property/getall', ProductPropController.getAll) // одно свойство товара router.get('/product/:productId([0-9]+)/property/getone/:id([0-9]+)', ProductPropController.getOne) // создать свойство товара router.post( '/product/:productId([0-9]+)/property/create', authMiddleware, adminMiddleware, ProductPropController.create ) // обновить свойство товара router.put( '/product/:productId([0-9]+)/property/update/:id([0-9]+)', authMiddleware, adminMiddleware, ProductPropController.update ) // удалить свойство товара router.delete( '/product/:productId([0-9]+)/property/delete/:id([0-9]+)', authMiddleware, adminMiddleware, ProductPropController.delete ) export default router
Создадим новую модель models/ProductProp.js
:
import { ProductProp as ProductPropMapping } from './mapping.js' import { Product as ProductMapping } from './mapping.js' import AppError from '../errors/AppError.js' class ProductProp { async getAll(productId) { const product = await ProductMapping.findByPk(productId) if (!product) { throw new Error('Товар не найден в БД') } const properties = await ProductPropMapping.findAll({where: {productId}}) return properties } async getOne(productId, id) { const product = await ProductMapping.findByPk(productId) if (!product) { throw new Error('Товар не найден в БД') } const property = await ProductPropMapping.findOne({where: {productId, id}}) if (!property) { throw new Error('Свойство товара не найдено в БД') } return property } async create(productId, data) { const product = await ProductMapping.findByPk(productId) if (!product) { throw new Error('Товар не найден в БД') } const {name, value} = data const property = await ProductPropMapping.create({name, value, productId}) return property } async update(productId, id, data) { const product = await ProductMapping.findByPk(productId) if (!product) { throw new Error('Товар не найден в БД') } const property = await ProductPropMapping.findOne({where: {productId, id}}) if (!property) { throw new Error('Свойство товара не найдено в БД') } const {name = property.name, value = property.value} = data await property.update({name, value}) return property } async delete(productId, id) { const product = await ProductMapping.findByPk(productId) if (!product) { throw new Error('Товар не найден в БД') } const property = await ProductPropMapping.findOne({where: {productId, id}}) if (!property) { throw new Error('Свойство товара не найдено в БД') } await property.destroy() return property } } export default new ProductProp()
Создадим новый контроллер controllers/ProductProp.js
:
import ProductPropModel from '../models/ProductProp.js' import AppError from '../errors/AppError.js' class ProductProp { async getAll(req, res, next) { try { if (!req.params.productId) { throw new Error('Не указан id товара') } const properties = await ProductPropModel.getAll(req.params.productId) res.json(properties) } catch(e) { next(AppError.badRequest(e.message)) } } async getOne(req, res, next) { try { if (!req.params.productId) { throw new Error('Не указан id товара') } if (!req.params.id) { throw new Error('Не указано id свойства') } const property = await ProductPropModel.getOne(req.params.productId, req.params.id) res.json(property) } catch(e) { next(AppError.badRequest(e.message)) } } async create(req, res, next) { try { if (!req.params.productId) { throw new Error('Не указан id товара') } if (Object.keys(req.body).length === 0) { throw new Error('Нет данных для создания') } const property = await ProductPropModel.create(req.params.productId, req.body) res.json(property) } catch(e) { next(AppError.badRequest(e.message)) } } async update(req, res, next) { try { if (!req.params.productId) { throw new Error('Не указан id товара') } if (!req.params.id) { throw new Error('Не указано id свойства') } if (Object.keys(req.body).length === 0) { throw new Error('Нет данных для обновления') } const property = await ProductPropModel.update(req.params.productId, req.params.id, req.body) res.json(property) } catch(e) { next(AppError.badRequest(e.message)) } } async delete(req, res, next) { try { if (!req.params.productId) { throw new Error('Не указан id товара') } if (!req.params.id) { throw new Error('Не указано id свойства') } const property = await ProductPropModel.delete(req.params.productId, req.params.id) res.json(property) } catch(e) { next(AppError.badRequest(e.message)) } } } export default new ProductProp()
Для тестирования запросов создадим файл test/product_prop.http
. Чтобы упростить тестирование и не отправлять с запросами JWT-токен, закомментируем в роутере middleware authMiddleware
и adminMiddleware
.
// создать свойство товара router.post( '/:productId([0-9]+)/property/create', // authMiddleware, // adminMiddleware, ProductPropController.create ) // обновить свойство товара router.put( '/:productId([0-9]+)/property/update/:id([0-9]+)', // authMiddleware, // adminMiddleware, ProductPropController.update ) // удалить свойство товара router.delete( '/:productId([0-9]+)/property/delete/:id([0-9]+)', // authMiddleware, // adminMiddleware, ProductPropController.delete )
### Список всех свойств товара id=1 GET /api/product/1/property/getall HTTP/1.1 Host: localhost:7000 ### Получить одно свойство товара GET /api/product/1/property/getone/1 HTTP/1.1 Host: localhost:7000 ### Создать новое свойство товара POST /api/product/1/property/create HTTP/1.1 Host: localhost:7000 Content-type: application/json; charset=utf-8 { "name": "Новое свойство товара", "value": "Значение свойства товара" } ### Обновить свойство товара PUT /api/product/1/property/update/3 HTTP/1.1 Host: localhost:7000 Content-type: application/json; charset=utf-8 { "name": "Свойство товара (updated)", "value": "Значение свойства (updated)" } ### Удалить свойство товара DELETE /api/product/1/property/delete/3 HTTP/1.1 Host: localhost:7000
Корзина покупателя
Нужна возможность создания, обновления и очистки корзины. После оформления заказа корзина очищается, но не удаляется — чтобы использовать в следующий раз вместо создания новой. При работе с корзиной на сервере будем отправлять клиенту заголовок Set-Cookie
с идентификатором корзины.
> npm install cookie-parser
Добавляем middleware для работы с cookie в server/index.js
:
import cookieParser from 'cookie-parser' /* .......... */ app.use(cookieParser(process.env.SECRET_KEY))
Создаем файл routes/basket.js
, где описываем новые маршруты:
import express from 'express' import BasketController from '../controllers/Basket.js' const router = new express.Router() router.get('/getone', BasketController.getOne) router.put('/product/:productId([0-9]+)/append/:quantity([0-9]+)', BasketController.append) router.put('/product/:productId([0-9]+)/increment/:quantity([0-9]+)', BasketController.increment) router.put('/product/:productId([0-9]+)/decrement/:quantity([0-9]+)', BasketController.decrement) router.put('/product/:productId([0-9]+)/remove', BasketController.remove) router.put('/clear', BasketController.clear) export default router
Редактируем routes/index.js
, чтобы новые маршруты стали доступны:
import express from 'express' import product from './product.js' import category from './category.js' import brand from './brand.js' import user from './user.js' import basket from './basket.js' const router = new express.Router() router.use('/product', product) router.use('/category', category) router.use('/brand', brand) router.use('/user', user) router.use('/basket', basket) export default router
Создаем контроллер controllers/Basket.js
:
import BasketModel from '../models/Basket.js' import ProductModel from '../models/Product.js' import AppError from '../errors/AppError.js' const maxAge = 60 * 60 * 1000 * 24 * 365 // один год const signed = true class Basket { async getOne(req, res, next) { try { let basket if (req.signedCookies.basketId) { basket = await BasketModel.getOne(parseInt(req.signedCookies.basketId)) } else { basket = await BasketModel.create() } res.cookie('basketId', basket.id, {maxAge, signed}) res.json(basket) } catch(e) { next(AppError.badRequest(e.message)) } } async append(req, res, next) { try { let basketId if (!req.signedCookies.basketId) { let created = await BasketModel.create() basketId = created.id } else { basketId = parseInt(req.signedCookies.basketId) } const {productId, quantity} = req.params const basket = await BasketModel.append(basketId, productId, quantity) res.cookie('basketId', basket.id, {maxAge, signed}) res.json(basket) } catch(e) { next(AppError.badRequest(e.message)) } } async increment(req, res, next) { try { let basketId if (!req.signedCookies.basketId) { let created = await BasketModel.create() basketId = created.id } else { basketId = parseInt(req.signedCookies.basketId) } const {productId, quantity} = req.params const basket = await BasketModel.increment(basketId, productId, quantity) res.cookie('basketId', basket.id, {maxAge, signed}) res.json(basket) } catch(e) { next(AppError.badRequest(e.message)) } } async decrement(req, res, next) { try { let basketId if (!req.signedCookies.basketId) { let created = await BasketModel.create() basketId = created.id } else { basketId = parseInt(req.signedCookies.basketId) } const {productId, quantity} = req.params const basket = await BasketModel.decrement(basketId, productId, quantity) res.cookie('basketId', basket.id, {maxAge, signed}) res.json(basket) } catch(e) { next(AppError.badRequest(e.message)) } } async remove(req, res, next) { try { let basketId if (!req.signedCookies.basketId) { let created = await BasketModel.create() basketId = created.id } else { basketId = parseInt(req.signedCookies.basketId) } const basket = await BasketModel.remove(basketId, req.params.productId) res.cookie('basketId', basket.id, {maxAge, signed}) res.json(basket) } catch(e) { next(AppError.badRequest(e.message)) } } async clear(req, res, next) { try { let basketId if (!req.signedCookies.basketId) { let created = await BasketModel.create() basketId = created.id } else { basketId = parseInt(req.signedCookies.basketId) } basket = await BasketModel.clear(basketId) res.cookie('basketId', basket.id, {maxAge, signed}) res.json(basket) } catch(e) { next(AppError.badRequest(e.message)) } } } export default new Basket()
Создаем модель models/Basket.js
:
import { Basket as BasketMapping } from './mapping.js' import { Product as ProductMapping } from './mapping.js' import { BasketProduct as BasketProductMapping } from './mapping.js' import AppError from '../errors/AppError.js' class Basket { async getOne(basketId) { let basket = await BasketMapping.findByPk(basketId, { attributes: ['id'], include: [ {model: ProductMapping, attributes: ['id', 'name', 'price']}, ], }) if (!basket) { basket = await BasketMapping.create() } return basket } async create() { const basket = await BasketMapping.create() return basket } async append(basketId, productId, quantity) { let basket = await BasketMapping.findByPk(basketId, { attributes: ['id'], include: [ {model: ProductMapping, attributes: ['id', 'name', 'price']}, ] }) if (!basket) { basket = await BasketMapping.create() } // проверяем, есть ли уже этот товар в корзине const basket_product = await BasketProductMapping.findOne({ where: {basketId, productId} }) if (basket_product) { // есть в корзине await basket_product.increment('quantity', {by: quantity}) } else { // нет в корзине await BasketProductMapping.create({basketId, productId, quantity}) } // обновим объект корзины, чтобы вернуть свежие данные await basket.reload() return basket } async increment(basketId, productId, quantity) { let basket = await BasketMapping.findByPk(basketId, { include: [{model: ProductMapping, as: 'products'}] }) if (!basket) { basket = await BasketMapping.create() } // проверяем, есть ли этот товар в корзине const basket_product = await BasketProductMapping.findOne({ where: {basketId, productId} }) if (basket_product) { await basket_product.increment('quantity', {by: quantity}) // обновим объект корзины, чтобы вернуть свежие данные await basket.reload() } return basket } async decrement(basketId, productId, quantity) { let basket = await BasketMapping.findByPk(basketId, { include: [{model: ProductMapping, as: 'products'}] }) if (!basket) { basket = await Basket.create() } // проверяем, есть ли этот товар в корзине const basket_product = await BasketProductMapping.findOne({ where: {basketId, productId} }) if (basket_product) { if (basket_product.quantity > quantity) { await basket_product.decrement('quantity', {by: quantity}) } else { await basket_product.destroy() } // обновим объект корзины, чтобы вернуть свежие данные await basket.reload() } return basket } async remove(basketId, productId) { let basket = await BasketMapping.findByPk(basketId, { include: [{model: ProductMapping, as: 'products'}] }) if (!basket) { basket = await Basket.create() } // проверяем, есть ли этот товар в корзине const basket_product = await BasketProductMapping.findOne({ where: {basketId, productId} }) if (basket_product) { await basket_product.destroy() // обновим объект корзины, чтобы вернуть свежие данные await basket.reload() } return basket } async clear(basketId) { let basket = await BasketMapping.findByPk(basketId, { include: [{model: ProductMapping, as: 'products'}] }) if (basket) { await BasketProductMapping.destroy({where: {basketId}}) // обновим объект корзины, чтобы вернуть свежие данные await basket.reload() } else { basket = await Basket.create() } return basket } async delete(basketId) { const basket = await BasketMapping.findByPk(basketId, { include: [{model: ProductMapping, as: 'products'}] }) if (!basket) { throw new Error('Корзина не найдена в БД') } await basket.destroy() return basket } } export default new Basket()
Для тестирования создаем файл test/basket.js
:
### Получить корзину GET /api/basket/getone HTTP/1.1 Host: localhost:7000 Cookie: basketId=5 ### Добавить в корзину PUT /api/basket/product/3/append/5 HTTP/1.1 Host: localhost:7000 Cookie: basketId=5 ### Удалить из корзины PUT /api/basket/product/1/remove HTTP/1.1 Host: localhost:7000 Cookie: basketId=5 ### Увеличить количество PUT /api/basket/product/1/increment/4 HTTP/1.1 Host: localhost:7000 Cookie: basketId=5 ### Уменьшить количество PUT /api/basket/product/1/decrement/2 HTTP/1.1 Host: localhost:7000 Cookie: basketId=5 ### Очистить корзину PUT /api/basket/clear HTTP/1.1 Host: localhost:7000 Cookie: basketId=5
Но перед тестированием изменяем настройку расширения REST Client для VS Code. Расширение запоминает cookie, полученные с сервера и дальше при каждом запросе отправляет их обратно. Нам это не нужно, мы будем сами отправлять cookie с идентификатором корзины на сервер.
{ "editor.fontSize": 15, .......... "rest-client.rememberCookiesForSubsequentRequests": false, }
Post Scriptum
Оказалось, что структура корзины, которую мы возвращаем на клиент, не слишком удобна для работы. Поэтому добавил функцию pretty
, чтобы данные корзины имели следующий вид:
{ "id": 38, "products": [ { "id": 29, "name": "Планшет раз", "price": 78901, "quantity": 1 }, { "id": 27, "name": "Смартфон раз", "price": 56789, "quantity": 2 } ] }
import { Basket as BasketMapping } from './mapping.js' import { Product as ProductMapping } from './mapping.js' import { BasketProduct as BasketProductMapping } from './mapping.js' import AppError from '../errors/AppError.js' const pretty = (basket) => { const data = {} data.id = basket.id data.products = [] if (basket.products) { data.products = basket.products.map(item => { return { id: item.id, name: item.name, price: item.price, quantity: item.basket_product.quantity } }) } return data } class Basket { /* .......... */ return pretty(basket) } async create() { /* .......... */ return pretty(basket) } async append(basketId, productId, quantity) { /* .......... */ return pretty(basket) } async increment(basketId, productId, quantity) { /* .......... */ return pretty(basket) } async decrement(basketId, productId, quantity) { /* .......... */ return pretty(basket) } async remove(basketId, productId) { /* .......... */ return pretty(basket) } async clear(basketId) { /* .......... */ return pretty(basket) } async delete(basketId) { /* .......... */ return pretty(basket) } } export default new Basket()
- Магазин на JavaScript, часть 19 из 19. Редактирование характеристик и рефакторинг приложения
- Магазин на JavaScript, часть 18 из 19. Панель управления: редактирование категорий и брендов
- Магазин на JavaScript, часть 17 из 19. Панель управления: список заказов, категорий и брендов
- Магазин на JavaScript, часть 15 из 19. Работа с заказами на сервере, оформление заказа
- Магазин на JavaScript, часть 14 из 19. Кнопка «Назад», страница товара, корзина покупателя
- Магазин на JavaScript, часть 13 из 19. Хранилище каталога, компонент витрины, кнопка «Назад»
- Магазин на JavaScript, часть12 из 19. Запросы на сервер, состояние приложения, Signup и Login
Поиск: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • База данных • Интернет магазин • Каталог товаров • Фреймворк • Корзина