Магазин на JavaScript, часть 4 из 19. CRUD для категорий и товаров, загрузка изображений

15.11.2021

Теги: BackendExpress.jsFrontendJavaScriptNode.jsORMReact.jsWeb-разработкаБазаДанныхИнтернетМагазинКаталогТоваровКорзинаФреймворк

Хорошо, теперь нам нужно реализовать все методы всех контроллеров, чтобы можно было создавать, обновлять, удалять и получать товары, категории, бренды и пользователей. Здесь нам поможет документация ORM-библиотеки Sequelize и ее методы create, update, save, destroy, reload, findOne, findByPk, findAll.

import { Product as ProductMapping } from '../models/mapping.js'
import AppError from '../errors/AppError.js'
import FileService from '../services/File.js'

class Product {
    async getAll(req, res, next) {
        try {
            const products = await ProductMapping.findAll()
            res.json(products)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async getOne(req, res, next) {
        try {
            if (!req.params.id) {
                throw new Error('Не указан id товара')
            }
            const product = await ProductMapping.findByPk(req.params.id)
            if (!product) {
                throw new Error('Товар не найден в БД')
            }
            res.json(product)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async create(req, res, next) {
        try {
            // поскольку image не допускает null, задаем пустую строку
            const {name, price, image = '', categoryId = null, brandId = null} = req.body
            const product = await ProductMapping.create({name, price, image, categoryId, brandId})
            res.json(product)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async update(req, res, next) {
        try {
            if (!req.params.id) {
                throw new Error('Не указан id товара')
            }
            const product = await ProductMapping.findByPk(req.params.id)
            if (!product) {
                throw new Error('Товар не найден в БД')
            }
            const name = req.body.name ?? product.name
            const price = req.body.price ?? product.price
            await product.update({name, price})
            res.json(product)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async delete(req, res, next) {
        try {
            if (!req.params.id) {
                throw new Error('Не указан id товара')
            }
            const product = await ProductMapping.findByPk(req.params.id)
            if (!product) {
                throw new Error('Товар не найден в БД')
            }
            await product.destroy()
            res.json(product)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }
}

export default new Product()

Прежде, чем двигаться дальше — создадим один товар, получим его из БД по идентификатору, потом обновим, а затем — удалим. Для этого редактируем файл test.http:

### Список всех товаров
GET /api/product/getall HTTP/1.1
Host: localhost:7000

### Получить один товар
GET /api/product/getone/1 HTTP/1.1
Host: localhost:7000

### Создать новый товар
POST /api/product/create HTTP/1.1
Host: localhost:7000
Content-type: application/json; charset=utf-8

{
    "name": "Первый товар",
    "price": 11111
}

### Обновить товар
PUT /api/product/update/1 HTTP/1.1
Host: localhost:7000
Content-type: application/json; charset=utf-8

{
    "name": "Первый товар (обновление)",
    "price": 22222
}

### Удалить товар
DELETE /api/product/delete/1 HTTP/1.1
Host: localhost:7000

Двигаемся дальше — реализуем методы контроллеров для категорий, брендов и пользователей.

import { Category as CategoryMapping } from '../models/mapping.js'
import AppError from '../errors/AppError.js'

class Category {
    async getAll(req, res, next) {
        try {
            const categories = await CategoryMapping.findAll()
            res.json(categories)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async getOne(req, res, next) {
        try {
            if (!req.params.id) {
                throw new Error('Не указан id категории')
            }
            const category = await CategoryMapping.findByPk(req.params.id)
            if (!category) {
                throw new Error('Категория не найдена в БД')
            }
            res.json(category)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async create(req, res, next) {
        try {
            const category = await CategoryMapping.create({name: req.body.name})
            res.json(category)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async update(req, res, next) {
        try {
            if (!req.params.id) {
                throw new Error('Не указан id категории')
            }
            const category = await CategoryMapping.findByPk(req.params.id)
            if (!category) {
                throw new Error('Категория не найдена в БД')
            }
            const name = req.body.name ?? category.name
            await category.update({name})
            res.json(category)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async delete(req, res, next) {
        try {
            if (!req.params.id) {
                throw new Error('Не указан id категории')
            }
            const category = await CategoryMapping.findByPk(req.params.id)
            if (!category) {
                throw new Error('Категория не найдена в БД')
            }
            await category.destroy()
            res.json(category)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }
}

export default new Category()
import { Brand as BrandMapping } from '../models/mapping.js'
import AppError from '../errors/AppError.js'

class Brand {
    async getAll(req, res, next) {
        try {
            const brands = await BrandMapping.findAll()
            res.json(brands)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async getOne(req, res, next) {
        try {
            if (!req.params.id) {
                throw new Error('Не указан id бренда')
            }
            const brand = await BrandMapping.findByPk(req.params.id)
            if (!brand) {
                throw new Error('Бренд не найден в БД')
            }
            res.json(brand)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async create(req, res, next) {
        try {
            const brand = await BrandMapping.create({name: req.body.name})
            res.json(brand)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async update(req, res, next) {
        try {
            if (!req.params.id) {
                throw new Error('Не указан id бренда')
            }
            const brand = await BrandMapping.findByPk(req.params.id)
            if (!brand) {
                throw new Error('Бренд не найден в БД')
            }
            const name = req.body.name ?? brand.name
            await brand.update({name})
            res.json(brand)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async delete(req, res, next) {
        try {
            if (!req.params.id) {
                throw new Error('Не указан id бренда')
            }
            const brand = await BrandMapping.findByPk(req.params.id)
            if (!brand) {
                throw new Error('Бренд не найден в БД')
            }
            await brand.destroy()
            res.json(brand)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }
}

export default new Brand()
import { User as UserMapping } from '../models/mapping.js'
import AppError from '../errors/AppError.js'

class User {
    async signup(req, res, next) {
        res.status(200).send('Регистрация пользователя')
    }

    async login(req, res, next) {
        res.status(200).send('Вход в личный кабинет')
    }

    async check(req, res, next) {
        res.status(200).send('Проверка авторизации')
    }

    async update(req, res, next) {
        try {
            if (!req.params.id) {
                throw new Error('Не указан id пользователя')
            }
            const user = await UserMapping.findByPk(req.params.id)
            if (!user) {
                throw new Error('Пользователь не найден в БД')
            }
            const name = req.body.name ?? user.name
            const password = req.body.password ?? user.password
            await user.update({name, password})
            res.json(user)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async delete(req, res, next) {
        try {
            if (!req.params.id) {
                throw new Error('Не указан id пользователя')
            }
            const user = await UserMapping.findByPk(req.params.id)
            if (!user) {
                throw new Error('Пользователь не найден в БД')
            }
            await user.destroy()
            res.json(user)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }
}

export default new User()

Загрузка изображений

Для загрузки изображений нам потребуется пакет express-fileupload — устанавливаем, импортируем и добавляем middleware в index.js:

> npm install express-fileupload
import config from 'dotenv/config'
import express from 'express'
import sequelize from './sequelize.js'
import * as mapping from './models/mapping.js'
import cors from 'cors'
import fileUpload from 'express-fileupload'
import router from './routes/index.js'
import ErrorHandler from './middleware/ErrorHandler.js'

const PORT = process.env.PORT || 5000

const app = express()
// Cross-Origin Resource Sharing
app.use(cors())
// middleware для работы с json
app.use(express.json())
// middleware для загрузки файлов
app.use(fileUpload())
// все маршруты приложения
app.use('/api', router)

// обработка ошибок
app.use(ErrorHandler)

const start = async () => {
    try {
        await sequelize.authenticate()
        await sequelize.sync()
        app.listen(PORT, () => console.log('Сервер запущен на порту', PORT))
    } catch(e) {
        console.log(e)
    }
}

start()

Давайте посмотрим, какие данные нам будут доступны при отправке POST-запроса с изображением на создание нового товара. Для этого выводим в консоль req.body и req.files.

import { Product as ProductMapping } from '../models/mapping.js'
import AppError from '../errors/AppError.js'

class Product {
    /* .......... */
    async create(req, res, next) {
        console.log(req.body)
        console.log(req.files)
        /* .......... */
    }
    /* .......... */
}

export default new Product()

Теперь подготовим POST-запрос — редактируем test.http и кладем в директорию shop/server файл picture.jpg:

### создание товара с изображением
POST /api/product/create HTTP/1.1
Host: localhost:7000
Content-Type: multipart/form-data; boundary=MultiPartFormDataBoundary

--MultiPartFormDataBoundary
Content-Disposition: form-data; name="name"

Название товара
--MultiPartFormDataBoundary
Content-Disposition: form-data; name="price"

11111
--MultiPartFormDataBoundary
Content-Disposition: form-data; name="image"; filename="picture.jpg"
Content-Type: image/jpeg

< ./picture.jpg
--MultiPartFormDataBoundary--
При тестировании обратите внимание, что название товара должно быть уникальным. То есть, при отправке каждого нового запроса нужно задать новое название товара.
Сервер запущен на порту 7000

{ name: 'Название товара', price: '11111' }

{
  image: {
    name: 'picture.jpg',
    data: <Buffer ff d8 ff e0 00 10 4a 46 49 46 00 01 01 01 00 60 00 60 00 00 ff db 00 43 00 ... 408440 more bytes>,
    size: 408490,
    encoding: '7bit',
    tempFilePath: '',
    truncated: false,
    mimetype: 'image/jpeg',
    md5: '0b37ad42c4efda0dc188762d72f63ae8',
    mv: [Function: mv]
  }
}

У нас есть размер, контрольная сумма, mimetype — а главное, есть функция mv(), с помощью которой можно поместить изображение в какую-то директорию проекта. Давайте сразу создадим такую директорию — пусть это будет shop/server/static. Для загрузки изображения создадим класс File в директории shop/server/services.

import * as uuid from 'uuid'
import * as path from 'path'

class File {
    save(file) {
        if (!file) return null
        const [, ext] = file.mimetype.split('/')
        const fileName = uuid.v4() + '.' + ext
        const filePath = path.resolve('static', fileName)
        file.mv(filePath)
        return fileName
    }
}

export default new File()

Здесь мы используем пакет uuid для генерации уникального имени файла, так что его надо установить:

> npm install uuid

В контроллере теперь будем использовать этот класс для загрузки изображений и сохранять имя файла в базу данных:

import { Product as ProductMapping } from '../models/mapping.js'
import AppError from '../errors/AppError.js'
import FileService from '../services/File.js'

class Product {
    /* .......... */
    async create(req, res, next) {
        try {
            // поскольку image не допускает null, задаем пустую строку
            const image = FileService.save(req.files?.image) ?? ''
            const {name, price, categoryId = null, brandId = null} = req.body
            const product = await ProductMapping.create({name, price, image, categoryId, brandId})
            res.json(product)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }
    /* .......... */
}

export default new Product()

Чтобы изображение можно было увидеть в браузере, добавляем еще одно middleware в файле index.js:

/* .......... */
const app = express()
// Cross-Origin Resource Sharing
app.use(cors())
// middleware для работы с json
app.use(express.json())
// middleware для статики (img, css)
app.use(express.static('static'))
// middleware для загрузки файлов
app.use(fileUpload())
// все маршруты приложения
app.use('/api', router)
/* .......... */

Теперь можно открыть загруженное изображение в браузере http://localhost:7000/9f7af171-f971-4b41-8e2b-d61737e5a2f4.jpeg.

Поиск: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • База данных • Интернет магазин • Каталог товаров • Корзина • Фреймворк

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.