Разработка интернет приложений javascript

Магазин на JavaScript, часть 1 из 19. Серверное приложение, база данных, ORM Sequelize

Простой интернет-магазин на Node.js (сервер) и React.js (клиент). Данные будем хранить в базе данных PostgreSQL. Для серверной части используем фреймворк Express.js. Приложение не имеет практической ценности, сделано с целью изучения.

Простой сервер

Создаем директорию проекта shop , внутри нее — еще две директории, server и client . Переходим в директорию shop/server , создаем проект:

Устанавливаем зависимости, которые нам потребуются в работе:

> npm install express pg pg-hstore sequelize cors dotenv

Чтобы отслеживать изменения в коде и перезапускать сервер, установим как dev-зависимость пакет nodemon :

> npm install nodemon --save-dev

Внесем изменения в файл package.json — добавим в него команду запуска сервера через nodemon и type:module (чтобы использовать import вместо require ).

 "name": "shop-server", "version": "1.0.0", "description": "Интернет магазин, сервер", "main": "index.js", "type": "module", "scripts":  "start": "node index.js", "start-dev": "nodemon index.js" >, "keywords": [ "Backend", "JavaScript", "Node.js", "Express.js" ], "author": "Евгений Токмаков", "license": "ISC", "devDependencies":  "nodemon": "^2.0.15" >, "dependencies":  "cors": "^2.8.5", "dotenv": "^10.0.0", "express": "^4.17.1", "pg": "^8.7.1", "pg-hstore": "^2.3.4", "sequelize": "^6.9.0" > >

Создаем файл index.js , добавляем в него следующий код:

import express from 'express' const PORT = 7000 const app = express() app.listen(PORT, () => console.log('Сервер запущен на порту', PORT))

Запускаем сервер в режиме разработки через nodemon :

> npm run start-dev [nodemon] 2.0.15 [nodemon] to restart at any time, enter `rs` [nodemon] watching path(s): *.* [nodemon] watching extensions: js,mjs,json [nodemon] starting `node index.js` Сервер запущен на порту 7000

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

И будем получать номер порта в index.js следующим образом:

import config from 'dotenv/config' import express from 'express' const PORT = process.env.PORT || 5000 const app = express() app.listen(PORT, () => console.log('Сервер запущен на порту', PORT))

База данных

Идем на сайт postgresql.org , в раздел Downloads — и скачиваем последнюю версию. Установка там простая, так что не будем на этом останавливаться. Вместе с базой данных будет установлен клиент pgAdmin для работы с сервером БД — запускаем его и создаем БД online_store .

Будем использовать ORM-библиотеку Sequelize, которая осуществляет сопоставление таблиц в БД и отношений между ними с классами моделей. Модели мы создадим чуть позже, а пока создаем файл sequelize.js , где укажем настройки подключения к серверу базы данных, сами настройки получим из файла .env .

import Sequelize> from 'sequelize' export default new Sequelize( process.env.DB_NAME, // база данных process.env.DB_USER, // пользователь process.env.DB_PASS, // пароль  dialect: 'postgres', host: process.env.DB_HOST, port: process.env.DB_PORT > )
PORT=7000 DB_HOST=localhost DB_NAME=online_store DB_USER=postgres DB_PASS=qwerty DB_PORT=5432

Вносим изменения в index.js , чтобы перед запуском сервера установить соединение с базой данных:

import config from 'dotenv/config' import express from 'express' import sequelize from './sequelize.js' const PORT = process.env.PORT || 5000 const app = express() const start = async () =>  try  await sequelize.authenticate() await sequelize.sync() app.listen(PORT, () => console.log('Сервер запущен на порту', PORT)) > catch(e)  console.log(e) > > start()

Метод sync() синхронизирует структуру базы данных с определением моделей. Например, если для какой-то модели отсутствует соответствующая таблица в БД, то эта таблица создается.

ORM (Object-relational mapping)

import sequelize from '../sequelize.js' import database from 'sequelize' const  DataTypes > = database /* * Описание моделей */ // модель «Пользователь», таблица БД «users» const User = sequelize.define('user',  id: type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true>, email: type: DataTypes.STRING, unique: true>, password: type: DataTypes.STRING>, role: type: DataTypes.STRING, defaultValue: 'USER'>, >) // модель «Корзина», таблица БД «baskets» const Basket = sequelize.define('basket',  id: type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true>, >) // связь между корзиной и товаром через промежуточную таблицу «basket_products» // у этой таблицы будет составной первичный ключ (basket_id + product_id) const BasketProduct = sequelize.define('basket_product',  quantity: type: DataTypes.INTEGER, defaultValue: 1>, >) // модель «Товар», таблица БД «products» const Product = sequelize.define('product',  id: type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true>, name: type: DataTypes.STRING, unique: true, allowNull: false>, price: type: DataTypes.INTEGER, allowNull: false>, rating: type: DataTypes.INTEGER, defaultValue: 0>, image: type: DataTypes.STRING, allowNull: false>, >) // модель «Категория», таблица БД «categories» const Category = sequelize.define('category',  id: type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true>, name: type: DataTypes.STRING, unique: true, allowNull: false>, >) // модель «Бренд», таблица БД «brands» const Brand = sequelize.define('brand',  id: type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true>, name: type: DataTypes.STRING, unique: true, allowNull: false>, >) // связь между товаром и пользователем через промежуточную таблицу «rating» // у этой таблицы будет составной первичный ключ (product_id + user_id) const Rating = sequelize.define('rating',  rate: type: DataTypes.INTEGER, allowNull: false>, >) // свойства товара, у одного товара может быть много свойств const ProductProp = sequelize.define('product_prop',  id: type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true>, name: type: DataTypes.STRING, allowNull: false>, value: type: DataTypes.STRING, allowNull: false>, >) /* * Описание связей */ // связь many-to-many товаров и корзин через промежуточную таблицу basket_products; // товар может быть в нескольких корзинах, в корзине может быть несколько товаров Basket.belongsToMany(Product,  through: BasketProduct, onDelete: 'CASCADE' >) Product.belongsToMany(Basket,  through: BasketProduct, onDelete: 'CASCADE' >) // super many-to-many https://sequelize.org/master/manual/advanced-many-to-many.html // это обеспечит возможность любых include при запросах findAll, findOne, findByPk Basket.hasMany(BasketProduct) BasketProduct.belongsTo(Basket) Product.hasMany(BasketProduct) BasketProduct.belongsTo(Product) // связь категории с товарами: в категории может быть несколько товаров, но // каждый товар может принадлежать только одной категории Category.hasMany(Product, onDelete: 'RESTRICT'>) Product.belongsTo(Category) // связь бренда с товарами: у бренда может быть много товаров, но каждый товар // может принадлежать только одному бренду Brand.hasMany(Product, onDelete: 'RESTRICT'>) Product.belongsTo(Brand) // связь many-to-many товаров и пользователей через промежуточную таблицу rating; // за один товар могут проголосовать несколько зарегистрированных пользователей, // один пользователь может проголосовать за несколько товаров Product.belongsToMany(User, through: Rating, onDelete: 'CASCADE'>) User.belongsToMany(Product, through: Rating, onDelete: 'CASCADE'>) // super many-to-many https://sequelize.org/master/manual/advanced-many-to-many.html // это обеспечит возможность любых include при запросах findAll, findOne, findByPk Product.hasMany(Rating) Rating.belongsTo(Product) User.hasMany(Rating) Rating.belongsTo(User) // связь товара с его свойствами: у товара может быть несколько свойств, но // каждое свойство связано только с одним товаром Product.hasMany(ProductProp, as: 'props', onDelete: 'CASCADE'>) ProductProp.belongsTo(Product) export  User, Basket, Product, Category, Brand, Rating, BasketProduct, ProductProp, Order, OrderItem >

В index.js импортируем модели, чтобы при вызове метода sync() были созданы все таблицы:

/* . */ import * as mapping from './models/mapping.js'; /* . */
-- Дамп структуры для таблицы public.baskets CREATE TABLE IF NOT EXISTS "baskets" ( "id" INTEGER NOT NULL DEFAULT 'nextval(. )', "created_at" TIMESTAMPTZ NOT NULL, "updated_at" TIMESTAMPTZ NOT NULL, PRIMARY KEY ("id") ) -- Дамп структуры для таблицы public.basket_products CREATE TABLE IF NOT EXISTS "basket_products" ( "quantity" INTEGER NULL DEFAULT '1', "created_at" TIMESTAMPTZ NOT NULL, "updated_at" TIMESTAMPTZ NOT NULL, "basket_id" INTEGER NOT NULL, "product_id" INTEGER NOT NULL, PRIMARY KEY ("basket_id", "product_id"), CONSTRAINT "basket_products_basket_id_fkey" FOREIGN KEY ("basket_id") REFERENCES "public"."baskets" ("id") ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT "basket_products_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "public"."products" ("id") ON UPDATE CASCADE ON DELETE CASCADE ); -- Дамп структуры для таблицы public.brands CREATE TABLE IF NOT EXISTS "brands" ( "id" INTEGER NOT NULL DEFAULT 'nextval(. )', "name" VARCHAR(255) NOT NULL, "created_at" TIMESTAMPTZ NOT NULL, "updated_at" TIMESTAMPTZ NOT NULL, PRIMARY KEY ("id"), UNIQUE INDEX "brands_name_key" ("name") ); -- Дамп структуры для таблицы public.categories CREATE TABLE IF NOT EXISTS "categories" ( "id" INTEGER NOT NULL DEFAULT 'nextval(. )', "name" VARCHAR(255) NOT NULL, "created_at" TIMESTAMPTZ NOT NULL, "updated_at" TIMESTAMPTZ NOT NULL, PRIMARY KEY ("id"), UNIQUE INDEX "categories_name_key" ("name") ); -- Дамп структуры для таблицы public.products CREATE TABLE IF NOT EXISTS "products" ( "id" INTEGER NOT NULL DEFAULT 'nextval(. )', "name" VARCHAR(255) NOT NULL, "price" INTEGER NOT NULL, "rating" INTEGER NULL DEFAULT '0', "image" VARCHAR(255) NOT NULL, "created_at" TIMESTAMPTZ NOT NULL, "updated_at" TIMESTAMPTZ NOT NULL, "category_id" INTEGER NULL DEFAULT NULL, "brand_id" INTEGER NULL DEFAULT NULL, PRIMARY KEY ("id"), UNIQUE INDEX "products_name_key" ("name"), CONSTRAINT "products_brand_id_fkey" FOREIGN KEY ("brand_id") REFERENCES "public"."brands" ("id") ON UPDATE CASCADE ON DELETE RESTRICT, CONSTRAINT "products_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "public"."categories" ("id") ON UPDATE CASCADE ON DELETE RESTRICT ); -- Дамп структуры для таблицы public.product_props CREATE TABLE IF NOT EXISTS "product_props" ( "id" INTEGER NOT NULL DEFAULT 'nextval(. )', "name" VARCHAR(255) NOT NULL, "value" VARCHAR(255) NOT NULL, "created_at" TIMESTAMPTZ NOT NULL, "updated_at" TIMESTAMPTZ NOT NULL, "product_id" INTEGER NULL DEFAULT NULL, PRIMARY KEY ("id"), CONSTRAINT "product_props_product_id_fkey" FOREIGN KEY ("product_id" REFERENCES "public"."products" ("id") ON UPDATE CASCADE ON DELETE CASCADE ); -- Дамп структуры для таблицы public.ratings CREATE TABLE IF NOT EXISTS "ratings" ( "rate" INTEGER NOT NULL, "created_at" TIMESTAMPTZ NOT NULL, "updated_at" TIMESTAMPTZ NOT NULL, "product_id" INTEGER NOT NULL, "user_id" INTEGER NOT NULL, PRIMARY KEY ("product_id", "user_id"), CONSTRAINT "ratings_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "public"."products" ("id") ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT "ratings_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id") ON UPDATE CASCADE ON DELETE CASCADE ); -- Дамп структуры для таблицы public.users CREATE TABLE IF NOT EXISTS "users" ( "id" INTEGER NOT NULL DEFAULT 'nextval(. )', "email" VARCHAR(255) NULL DEFAULT NULL, "password" VARCHAR(255) NULL DEFAULT NULL, "role" VARCHAR(255) NULL DEFAULT 'USER', "created_at" TIMESTAMPTZ NOT NULL, "updated_at" TIMESTAMPTZ NOT NULL, PRIMARY KEY ("id"), UNIQUE INDEX "users_email_key" ("email") );

Кроме описанных нами полей у каждой таблицы будут созданы поля createdAt и updatedAt типа datetime — это время создания и последнего обновления строки в таблице. Если эти поля не нужны — редактируем файл sequelize.js :

import Sequelize> from 'sequelize' export default new Sequelize( process.env.DB_NAME, // база данных process.env.DB_USER, // пользователь process.env.DB_PASS, // пароль  dialect: 'postgres', host: process.env.DB_HOST, port: process.env.DB_PORT, define:  underscored: true, // использовать snake_case вместо camelCase для полей таблиц БД timestamps: false, // не добавлять поля created_at и updated_at при создании таблиц > > )

Источник

Читайте также:  Среда программирование на бейсике
Оцените статью