create
This commit is contained in:
commit
edb8978fdc
195
API.rest
Executable file
195
API.rest
Executable file
@ -0,0 +1,195 @@
|
|||||||
|
|
||||||
|
// STOCKS
|
||||||
|
|
||||||
|
// Create Product:
|
||||||
|
POST http://localhost/stocks/api/product/create
|
||||||
|
content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"plu": 11297,
|
||||||
|
"name": "Apple"
|
||||||
|
}
|
||||||
|
###
|
||||||
|
|
||||||
|
// Create Shop:
|
||||||
|
POST http://localhost/stocks/api/shop/create
|
||||||
|
content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Магнит"
|
||||||
|
}
|
||||||
|
###
|
||||||
|
|
||||||
|
// Create Stocks:
|
||||||
|
POST http://localhost/stocks/api/stocks/create
|
||||||
|
content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"plu": 1111,
|
||||||
|
"shop_id": 4,
|
||||||
|
"in_stock": 3789,
|
||||||
|
"in_order": 5000
|
||||||
|
}
|
||||||
|
###
|
||||||
|
|
||||||
|
// Increase Stocks by plu/shop_id:
|
||||||
|
PATCH http://localhost/stocks/api/stocks/increase
|
||||||
|
content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
//"id": 5,
|
||||||
|
"plu": "1111",
|
||||||
|
"shop_id": 1,
|
||||||
|
"increase_shelf": 12,
|
||||||
|
"increase_order": 15
|
||||||
|
}
|
||||||
|
###
|
||||||
|
|
||||||
|
// [Optional] Increase Stocks by id:
|
||||||
|
PATCH http://localhost/stocks/api/stocks/increase
|
||||||
|
content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"increase_shelf": 12,
|
||||||
|
"increase_order": 15
|
||||||
|
}
|
||||||
|
###
|
||||||
|
|
||||||
|
|
||||||
|
// Decrease Stocks by plu/shop_id:
|
||||||
|
PATCH http://localhost/stocks/api/stocks/decrease
|
||||||
|
content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
// "id": 5, // Optional OR:
|
||||||
|
"plu": 1112,
|
||||||
|
"shop_id": 3,
|
||||||
|
"decrease_shelf": 12,
|
||||||
|
"decrease_order": 15
|
||||||
|
}
|
||||||
|
###
|
||||||
|
|
||||||
|
// [Optional] Decrease Stocks by id:
|
||||||
|
PATCH http://localhost/stocks/api/stocks/decrease
|
||||||
|
content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"decrease_shelf": 12,
|
||||||
|
"decrease_order": 15
|
||||||
|
}
|
||||||
|
###
|
||||||
|
|
||||||
|
|
||||||
|
// Get Stocks:
|
||||||
|
|
||||||
|
// plu=1111
|
||||||
|
// shop_id=1
|
||||||
|
// shelf_from=0
|
||||||
|
// shelf_to=3800
|
||||||
|
// order_from=0
|
||||||
|
// order_to=10000
|
||||||
|
// page=1
|
||||||
|
// page_size=2
|
||||||
|
|
||||||
|
// [ALL PARAMS]:
|
||||||
|
GET http://localhost/stocks/api/stocks?plu=1111&shop_id=1&shelf_from=0&shelf_to=3800&order_from=0&order_to=10000&page=1&page_size=2
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
// [SANDBOX]:
|
||||||
|
GET http://localhost/stocks/api/stocks?shop_id=1&page=1&page_size=1
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
|
||||||
|
// Get Products:
|
||||||
|
|
||||||
|
// plu=1111
|
||||||
|
// name=appl
|
||||||
|
// page=1
|
||||||
|
// page_size=2
|
||||||
|
|
||||||
|
// [ALL PARAMS]:
|
||||||
|
GET http://localhost/stocks/api/products?plu=&name=ябл&page=1&page_size=2
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
|
||||||
|
//===============================================================================================================================
|
||||||
|
|
||||||
|
// Service 2
|
||||||
|
// Get History:
|
||||||
|
|
||||||
|
// plu=1111
|
||||||
|
// shop_id=1
|
||||||
|
// action=decreaseStocks
|
||||||
|
// date_from=1731906196 (Unix Timestamp)
|
||||||
|
// date_to=1731906196
|
||||||
|
// page=1
|
||||||
|
// page_size=3
|
||||||
|
|
||||||
|
|
||||||
|
// [ALL PARAMS]:
|
||||||
|
GET http://localhost/history/api/history?plu=1111&shop_id=1&action=create&date_from=105653336&date_to=2034634345&page=1&page_size=10
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
// [ALL]:
|
||||||
|
GET http://localhost/history/api/history?page=1&page_size=10
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
// [SANDBOX]:
|
||||||
|
GET http://localhost/history/api/history?&page=1&page_size=30&date_to=1731906190
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
|
||||||
|
//===============================================================================================================================
|
||||||
|
|
||||||
|
// Users
|
||||||
|
|
||||||
|
// Create User:
|
||||||
|
POST http://localhost/users/api/users/create
|
||||||
|
content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"firstName": "Иван",
|
||||||
|
"lastName": "Иванов",
|
||||||
|
"birthday": "2006-11-19 19:00:00.000 +00:00",
|
||||||
|
"sex": 1,
|
||||||
|
"problems": true
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
// Delete All Users
|
||||||
|
DELETE http://localhost/users/api/users/delete-all
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
// Create All Users
|
||||||
|
POST http://localhost/users/api/users/create-all
|
||||||
|
content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1000000
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
// Get Users
|
||||||
|
GET http://localhost/users/api/users?page=1&pageSize=10
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
// Reset Problems:
|
||||||
|
PATCH http://localhost/users/api/users/reset-problems
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
POST http://localhost/users/api/rand-problems
|
||||||
|
|
||||||
|
###
|
1
LICENSE.md
Executable file
1
LICENSE.md
Executable file
@ -0,0 +1 @@
|
|||||||
|
Free
|
40
README.md
Executable file
40
README.md
Executable file
@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
### Тестовое задание
|
||||||
|
Репозиторий: `em-shop`
|
||||||
|
|
||||||
|
Исполнитель: `Leonid Ost`
|
||||||
|
Email: `4267@mail.ru`
|
||||||
|
Telegram: `@leo4267`
|
||||||
|
Site: `checkerwars.com`
|
||||||
|
|
||||||
|
Описание всех API находится в файле `API.rest`
|
||||||
|
|
||||||
|
### Stocks
|
||||||
|
Path API `http://localhost/stocks/api/`
|
||||||
|
|
||||||
|
### History
|
||||||
|
Path API `http://localhost/history/api/`
|
||||||
|
|
||||||
|
### User Swagger
|
||||||
|
Path API `http://localhost/users/api/`
|
||||||
|
Swagger: `http://localhost/users/api/docs/`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Traefik dashboard
|
||||||
|
Path: `http://localhost/dashboard/`
|
||||||
|
User: admin
|
||||||
|
Password: admin
|
||||||
|
|
||||||
|
|
||||||
|
### RabbitMQ dashboard
|
||||||
|
Path: `http://localhost:15672/`
|
||||||
|
User: rabbit
|
||||||
|
Password: 5jbya3ptfrezyop6gy8w
|
||||||
|
|
||||||
|
# PostgreSQL
|
||||||
|
Host: postgres
|
||||||
|
Port: 5432
|
||||||
|
User: postgres
|
||||||
|
DB: postgres
|
||||||
|
Password: 2wroxrnr8fdxicvw2nsd
|
5
history/.dockerignore
Executable file
5
history/.dockerignore
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
/node_modules
|
||||||
|
docker-compose.yml
|
||||||
|
docker-compose.dev.yml
|
||||||
|
run.dev.sh
|
||||||
|
run.prod.sh
|
18
history/.gitignore
vendored
Executable file
18
history/.gitignore
vendored
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
23
history/Dockerfile
Executable file
23
history/Dockerfile
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
# develop stage
|
||||||
|
FROM node:22.11.0-alpine3.20 AS develop-stage
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm config set fund false --location=global
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
|
||||||
|
# build stage
|
||||||
|
FROM develop-stage AS build-stage
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
|
# production stage
|
||||||
|
FROM node:22.11.0-alpine3.20 AS production-stage
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build-stage /app/node_modules /app/node_modules
|
||||||
|
COPY --from=build-stage /app/dist /app/dist
|
||||||
|
COPY package.json ./
|
||||||
|
ENV NODE_PATH=./node_modules
|
||||||
|
CMD ["node", "dist/index.js"]
|
||||||
|
EXPOSE 3002
|
115
history/README.md
Executable file
115
history/README.md
Executable file
@ -0,0 +1,115 @@
|
|||||||
|
|
||||||
|
### History
|
||||||
|
|
||||||
|
Backend history v0.1
|
||||||
|
Repository name: `em-shop`
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
*cheat sheet:*
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Запуск сервера в production mode:
|
||||||
|
sudo bash run.prod.sh
|
||||||
|
sudo docker compose up -d --build --force-recreate
|
||||||
|
|
||||||
|
# Запуск сервера в development mode:
|
||||||
|
sudo bash run.dev.sh
|
||||||
|
sudo docker compose -f docker-compose.dev.yml up --build --force-recreate
|
||||||
|
|
||||||
|
# Подключение к консоли контейнера:
|
||||||
|
sudo docker exec -it history sh
|
||||||
|
sudo docker exec -it history-dev sh
|
||||||
|
|
||||||
|
sudo docker start history
|
||||||
|
sudo docker stop history
|
||||||
|
|
||||||
|
sudo docker start history-dev
|
||||||
|
sudo docker stop history-dev
|
||||||
|
|
||||||
|
```
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
#### Общие сведения
|
||||||
|
|
||||||
|
Для успешного запуска и функционирования требуются следующий файл конфигурации:
|
||||||
|
```yml
|
||||||
|
env_file:
|
||||||
|
- ./config.env
|
||||||
|
```
|
||||||
|
|
||||||
|
В `config.env` указываюся переменные:
|
||||||
|
(Используйте эти значения только для разработки)
|
||||||
|
|
||||||
|
```conf
|
||||||
|
PORT=3002
|
||||||
|
URL=/history/api
|
||||||
|
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_DB=postgres
|
||||||
|
POSTGRES_PASSWORD=2wroxrnr8fdxicvw2nsd
|
||||||
|
|
||||||
|
RABBITMQ_HOST=rabbitmq
|
||||||
|
RABBITMQ_PORT=5672
|
||||||
|
RABBITMQ_USER=rabbit
|
||||||
|
RABBITMQ_PASSWORD=5jbya3ptfrezyop6gy8w
|
||||||
|
RABBITMQ_VHOST=vrabbit
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Production mode:
|
||||||
|
Режим запуска сервера по умолчанию.
|
||||||
|
Конфигурация описана в `docker-compose.yml`
|
||||||
|
Запуск сервера осуществляется простой командой:
|
||||||
|
|
||||||
|
`docker compose up -d`
|
||||||
|
|
||||||
|
После изменений в образе, запуск производится с пересборкой образа:
|
||||||
|
|
||||||
|
`docker compose up -d --build --force-recreate`
|
||||||
|
|
||||||
|
Для быстрого удобного запуска предусмотрен `скрипт run.prod.sh`, который выполняет команду выше.
|
||||||
|
|
||||||
|
*Пояснения:*
|
||||||
|
Запуск производится по средствам выполнения в образе команды:
|
||||||
|
|
||||||
|
`command: ["node", "src/index.js"]`
|
||||||
|
|
||||||
|
|
||||||
|
#### Development mode:
|
||||||
|
Запуск сервера в режиме разработки. Требуется указать соответствующий образ:
|
||||||
|
|
||||||
|
`docker compose -f docker-compose.dev.yml up -d`
|
||||||
|
|
||||||
|
После изменений в образе, запуск производится с пересборкой образа:
|
||||||
|
|
||||||
|
`docker compose -f docker-compose.dev.yml up -d --build --force-recreate`
|
||||||
|
|
||||||
|
Для быстрого удобного запуска предусмотрен скрипт `run.dev.sh`, который выполняет команду выше.
|
||||||
|
|
||||||
|
*Пояснения:*
|
||||||
|
Запуск производится по средствам выполнения в образе команды:
|
||||||
|
|
||||||
|
`command: sh -c "npm install && npm run start:dev"`
|
||||||
|
|
||||||
|
`start:dev` описана в `package.json` и содержит следующую команду:
|
||||||
|
|
||||||
|
`nodemon ./src/index.js`
|
||||||
|
|
||||||
|
причем, перед запуском сервера выполняется установка `npm install`, что необходимо для корректного прозрачного отображения папки
|
||||||
|
`node_modules` в каталоге, что бы в процессе разработки у IDE был доступ к этой папке для корректного анализа,
|
||||||
|
и подсветки кода в IDE.
|
||||||
|
|
||||||
|
Так же, весь каталог с репозиторием монтируется в режиме записи в как корневой `/app` в Docker контейнере:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
volumes:
|
||||||
|
- .:/app:rw
|
||||||
|
```
|
||||||
|
|
||||||
|
что необходимо для внесения изменений в код без пересборки контейнера.
|
||||||
|
|
||||||
|
В `profuction` режиме сборка осуществляется обычно, `node_modules` и все файлы находятся внутри образа, никаких дополнительных команд, volumes, не используется.
|
||||||
|
|
14
history/config.env
Executable file
14
history/config.env
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
PORT=3002
|
||||||
|
URL=/history/api
|
||||||
|
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_DB=postgres
|
||||||
|
POSTGRES_PASSWORD=2wroxrnr8fdxicvw2nsd
|
||||||
|
|
||||||
|
RABBITMQ_HOST=rabbitmq
|
||||||
|
RABBITMQ_PORT=5672
|
||||||
|
RABBITMQ_USER=rabbit
|
||||||
|
RABBITMQ_PASSWORD=5jbya3ptfrezyop6gy8w
|
||||||
|
RABBITMQ_VHOST=vrabbit
|
30
history/docker-compose.dev.yml
Executable file
30
history/docker-compose.dev.yml
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
services:
|
||||||
|
history-dev:
|
||||||
|
container_name: history-dev
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: 'develop-stage'
|
||||||
|
env_file:
|
||||||
|
- ./config.env
|
||||||
|
environment:
|
||||||
|
NODE_ENV: development
|
||||||
|
volumes:
|
||||||
|
- .:/app:rw
|
||||||
|
command: sh -c "npm install && npm run start:dev"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- proxynet
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.apihistory.rule=PathPrefix(`/history/api`)"
|
||||||
|
- "traefik.http.routers.apihistory.entrypoints=http"
|
||||||
|
- "traefik.http.routers.apihistory.service=apihistory-service"
|
||||||
|
- "traefik.http.services.apihistory-service.loadbalancer.server.port=3002"
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "1m"
|
||||||
|
networks:
|
||||||
|
proxynet:
|
||||||
|
external: true
|
||||||
|
|
28
history/docker-compose.yml
Executable file
28
history/docker-compose.yml
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
services:
|
||||||
|
history:
|
||||||
|
container_name: history
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: 'production-stage'
|
||||||
|
env_file:
|
||||||
|
- ./config.env
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
command: ["node", "dist/index.js"]
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- proxynet
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.apihistory.rule=PathPrefix(`/history/api`)"
|
||||||
|
- "traefik.http.routers.apihistory.entrypoints=http"
|
||||||
|
- "traefik.http.routers.apihistory.service=apihistory-service"
|
||||||
|
- "traefik.http.services.apihistory-service.loadbalancer.server.port=3002"
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "1m"
|
||||||
|
networks:
|
||||||
|
proxynet:
|
||||||
|
external: true
|
||||||
|
|
2016
history/package-lock.json
generated
Executable file
2016
history/package-lock.json
generated
Executable file
File diff suppressed because it is too large
Load Diff
31
history/package.json
Executable file
31
history/package.json
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "history",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "History api server",
|
||||||
|
"main": "index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"start": "ts-node ./src/index.ts",
|
||||||
|
"start:dev": "nodemon --exec ts-node ./src/index.ts",
|
||||||
|
"build": "tsc"
|
||||||
|
},
|
||||||
|
"author": "Leonid",
|
||||||
|
"license": "Free",
|
||||||
|
"dependencies": {
|
||||||
|
"amqplib": "^0.10.4",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^4.21.1",
|
||||||
|
"express-validator": "^7.2.0",
|
||||||
|
"pg": "^8.13.1",
|
||||||
|
"sequelize": "^6.37.5",
|
||||||
|
"typescript": "^5.6.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.1.7",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/node": "^22.9.0",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"@types/sequelize": "^4.28.20"
|
||||||
|
}
|
||||||
|
}
|
8
history/run.dev.sh
Executable file
8
history/run.dev.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ "$(id -u)" != "0" ]; then
|
||||||
|
echo -e "\033[31mThis script requires superuser rights.\033[0m"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo docker compose -f docker-compose.dev.yml up --build --force-recreate
|
8
history/run.prod.sh
Executable file
8
history/run.prod.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ "$(id -u)" != "0" ]; then
|
||||||
|
echo -e "\033[31mThis script requires superuser rights.\033[0m"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo docker compose up -d --build --force-recreate
|
33
history/src/controllers/history.controller.ts
Executable file
33
history/src/controllers/history.controller.ts
Executable file
@ -0,0 +1,33 @@
|
|||||||
|
import historyService from '../service/history.service';
|
||||||
|
import { validationResult } from 'express-validator';
|
||||||
|
import ApiError from '../exceptions/api.error';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
class HistoryController {
|
||||||
|
async getHistory(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
|
try {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return next(ApiError.BadRequest('Validation error', errors.array()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { shop_id, plu, action, date_from, date_to, page, page_size } = req.query;
|
||||||
|
|
||||||
|
const history = await historyService.getHistory({
|
||||||
|
shopId: shop_id ? parseInt(shop_id as string) : null,
|
||||||
|
plu: plu ? parseInt(plu as string) : null,
|
||||||
|
action: action ? action as string : null,
|
||||||
|
dateFrom: date_from ? parseInt(date_from as string) : null,
|
||||||
|
dateTo: date_to ? parseInt(date_to as string) : null,
|
||||||
|
page: parseInt(page as string),
|
||||||
|
pageSize: parseInt(page_size as string)
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(history);
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new HistoryController();
|
12
history/src/db.ts
Executable file
12
history/src/db.ts
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
import { Sequelize } from 'sequelize';
|
||||||
|
|
||||||
|
const sequelize = new Sequelize({
|
||||||
|
dialect: 'postgres',
|
||||||
|
host: process.env.POSTGRES_HOST,
|
||||||
|
port: Number(process.env.POSTGRES_PORT),
|
||||||
|
username: process.env.POSTGRES_USER,
|
||||||
|
database: process.env.POSTGRES_DB,
|
||||||
|
password: process.env.POSTGRES_PASSWORD
|
||||||
|
});
|
||||||
|
|
||||||
|
export default sequelize;
|
20
history/src/exceptions/api.error.ts
Executable file
20
history/src/exceptions/api.error.ts
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
class ApiError extends Error {
|
||||||
|
status: number;
|
||||||
|
errors: any[];
|
||||||
|
|
||||||
|
constructor(status: number, message: string, errors = []) {
|
||||||
|
super(message);
|
||||||
|
this.status = status;
|
||||||
|
this.errors = errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
static UnauthorizedError() {
|
||||||
|
return new ApiError(401, 'Пользователь не авторизован')
|
||||||
|
}
|
||||||
|
|
||||||
|
static BadRequest(message: string, errors = []) {
|
||||||
|
return new ApiError(400, message, errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiError;
|
56
history/src/index.ts
Executable file
56
history/src/index.ts
Executable file
@ -0,0 +1,56 @@
|
|||||||
|
// History v0.1
|
||||||
|
|
||||||
|
import http from 'http';
|
||||||
|
import express from 'express';
|
||||||
|
import sequelize from './db';
|
||||||
|
import rabbitMqService from './rabbitmq';
|
||||||
|
import router from './router/router';
|
||||||
|
import errorMiddleware from './middlewares/error.middleware';
|
||||||
|
import historyService from './service/history.service';
|
||||||
|
import { ConsumeMessage } from 'amqplib';
|
||||||
|
|
||||||
|
const PORT = Number(process.env.PORT) || 3000;
|
||||||
|
const app = express();
|
||||||
|
const server = http.createServer(app);
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(process.env.URL, router);
|
||||||
|
app.use(errorMiddleware);
|
||||||
|
|
||||||
|
const start = async () => {
|
||||||
|
try {
|
||||||
|
await sequelize.authenticate();
|
||||||
|
console.log('Connection has been established successfully');
|
||||||
|
|
||||||
|
await sequelize.sync().then(() => {
|
||||||
|
console.log('Database and tables created!');
|
||||||
|
});
|
||||||
|
|
||||||
|
await rabbitMqService.connectRabbitMq('logs');
|
||||||
|
const channel = await rabbitMqService.getChannel();
|
||||||
|
await channel.consume(
|
||||||
|
'logs', async (msg: ConsumeMessage) => {
|
||||||
|
try {
|
||||||
|
const logEntry = JSON.parse(msg.content.toString());
|
||||||
|
await historyService.createHistory(
|
||||||
|
logEntry.action,
|
||||||
|
logEntry.shop_id,
|
||||||
|
logEntry.plu,
|
||||||
|
logEntry.oldData,
|
||||||
|
logEntry.newData
|
||||||
|
);
|
||||||
|
channel.ack(msg);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing message:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ noAck: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
server.listen(PORT, () => console.log(`Server started on PORT = ${PORT}`));
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start();
|
15
history/src/middlewares/auth.middleware.ts
Executable file
15
history/src/middlewares/auth.middleware.ts
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
import ApiError from '../exceptions/api.error'
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
export default function (req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
if (false) {
|
||||||
|
return next(ApiError.UnauthorizedError());
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
} catch (e) {
|
||||||
|
return next(ApiError.UnauthorizedError());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
10
history/src/middlewares/error.middleware.ts
Executable file
10
history/src/middlewares/error.middleware.ts
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
import ApiError from '../exceptions/api.error';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
export default function (err: Error, req: Request, res: Response, next: NextFunction) {
|
||||||
|
console.log(err);
|
||||||
|
if (err instanceof ApiError) {
|
||||||
|
res.status(err.status).json({ message: err.message, errors: err.errors });
|
||||||
|
}
|
||||||
|
res.status(500).json({message: 'Ошибка сервера'});
|
||||||
|
};
|
34
history/src/models/history.model.ts
Executable file
34
history/src/models/history.model.ts
Executable file
@ -0,0 +1,34 @@
|
|||||||
|
import { Sequelize, DataTypes, Model, Optional } from 'sequelize';
|
||||||
|
import sequelize from '../db';
|
||||||
|
|
||||||
|
interface HistoryAttributes {
|
||||||
|
id: number;
|
||||||
|
action: string;
|
||||||
|
shop_id: number | null;
|
||||||
|
plu: number | null;
|
||||||
|
old_data: JSON | null;
|
||||||
|
new_data: JSON | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class History extends Model<HistoryAttributes> implements HistoryAttributes {
|
||||||
|
public id: number;
|
||||||
|
public action: string;
|
||||||
|
public shop_id: number | null;
|
||||||
|
public plu: number | null;
|
||||||
|
public old_data: JSON | null;
|
||||||
|
public new_data: JSON | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
History.init({
|
||||||
|
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
|
||||||
|
action: { type: DataTypes.STRING, defaultValue: 'undefined' },
|
||||||
|
shop_id: { type: DataTypes.INTEGER, defaultValue: null },
|
||||||
|
plu: { type: DataTypes.INTEGER, defaultValue: null },
|
||||||
|
old_data: { type: DataTypes.JSON, defaultValue: null },
|
||||||
|
new_data: { type: DataTypes.JSON, defaultValue: null }
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'history',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default History;
|
41
history/src/rabbitmq.ts
Executable file
41
history/src/rabbitmq.ts
Executable file
@ -0,0 +1,41 @@
|
|||||||
|
import amqp, { Channel, Connection } from "amqplib";
|
||||||
|
|
||||||
|
class RabbitMqService {
|
||||||
|
private channel: Channel | null = null;
|
||||||
|
private queue: string | null = null;
|
||||||
|
|
||||||
|
public async connectRabbitMq(queue: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const connection: Connection = await amqp.connect({
|
||||||
|
protocol: 'amqp',
|
||||||
|
hostname: process.env.RABBITMQ_HOST as string,
|
||||||
|
port: Number(process.env.RABBITMQ_PORT),
|
||||||
|
username: process.env.RABBITMQ_USER as string,
|
||||||
|
password: process.env.RABBITMQ_PASSWORD as string,
|
||||||
|
vhost: process.env.RABBITMQ_VHOST as string,
|
||||||
|
});
|
||||||
|
this.channel = await connection.createChannel();
|
||||||
|
await this.channel.assertQueue(queue);
|
||||||
|
this.queue = queue;
|
||||||
|
console.log("RabbitMQ connected");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error connecting to RabbitMQ", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getChannel(): Channel | null {
|
||||||
|
return this.channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendToQueue(data: any): void {
|
||||||
|
if (!this.channel || !this.queue) {
|
||||||
|
throw new Error("Channel or queue not initialized.");
|
||||||
|
}
|
||||||
|
this.channel.sendToQueue(this.queue, Buffer.from(JSON.stringify(data)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rabbitMqService = new RabbitMqService();
|
||||||
|
|
||||||
|
export default rabbitMqService;
|
15
history/src/router/router.ts
Executable file
15
history/src/router/router.ts
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import historyController from '../controllers/history.controller';
|
||||||
|
import authMiddleware from '../middlewares/auth.middleware';
|
||||||
|
import { getHistoryDataValidate } from './validators';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/history',
|
||||||
|
authMiddleware,
|
||||||
|
getHistoryDataValidate,
|
||||||
|
historyController.getHistory
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
26
history/src/router/validators.ts
Executable file
26
history/src/router/validators.ts
Executable file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
import { query } from 'express-validator';
|
||||||
|
|
||||||
|
// history:
|
||||||
|
export const getHistoryDataValidate = [
|
||||||
|
query('shop_id')
|
||||||
|
.toFloat()
|
||||||
|
.default(null),
|
||||||
|
query('plu')
|
||||||
|
.toFloat()
|
||||||
|
.default(null),
|
||||||
|
query('action')
|
||||||
|
.default(null),
|
||||||
|
query('date_from')
|
||||||
|
.toFloat()
|
||||||
|
.default(null),
|
||||||
|
query('date_to')
|
||||||
|
.toFloat()
|
||||||
|
.default(null),
|
||||||
|
query('page')
|
||||||
|
.toFloat()
|
||||||
|
.default(1),
|
||||||
|
query('page_size')
|
||||||
|
.toFloat()
|
||||||
|
.default(10),
|
||||||
|
];
|
67
history/src/service/history.service.ts
Executable file
67
history/src/service/history.service.ts
Executable file
@ -0,0 +1,67 @@
|
|||||||
|
import HistoryModel from '../models/history.model';
|
||||||
|
import { Op } from 'sequelize';
|
||||||
|
|
||||||
|
interface GetHistoryParams {
|
||||||
|
shopId?: number | null;
|
||||||
|
plu?: number | null;
|
||||||
|
action?: string | null;
|
||||||
|
dateFrom?: number | null;
|
||||||
|
dateTo?: number | null;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HistoryResponse {
|
||||||
|
history: HistoryModel[];
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
totalHistory: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class HistoryService {
|
||||||
|
async createHistory(action: string, shop_id: number, plu: number, old_data: JSON, new_data: JSON): Promise<HistoryModel> {
|
||||||
|
const history = await HistoryModel.create({ action, shop_id, plu, old_data, new_data });
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHistory({
|
||||||
|
shopId = null,
|
||||||
|
plu = null,
|
||||||
|
action = null,
|
||||||
|
dateFrom = null,
|
||||||
|
dateTo = null,
|
||||||
|
page = 1,
|
||||||
|
pageSize = 10
|
||||||
|
}: GetHistoryParams): Promise<HistoryResponse> {
|
||||||
|
const offset = (page - 1) * pageSize;
|
||||||
|
const limit = pageSize;
|
||||||
|
|
||||||
|
const where: any = {};
|
||||||
|
if (dateFrom) {
|
||||||
|
const formattedDateFrom = new Date(dateFrom * 1000).toISOString();
|
||||||
|
where.createdAt = { ...where.createdAt, [Op.gt]: formattedDateFrom };
|
||||||
|
}
|
||||||
|
if (dateTo) {
|
||||||
|
const formattedDateTo = new Date(dateTo * 1000).toISOString();
|
||||||
|
where.createdAt = { ...where.createdAt, [Op.lt]: formattedDateTo };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shopId) where.shop_id = shopId;
|
||||||
|
if (plu) where.plu = plu;
|
||||||
|
if (action) where.action = action;
|
||||||
|
|
||||||
|
const history = await HistoryModel.findAndCountAll({ limit, offset, where });
|
||||||
|
const totalPages = Math.ceil(history.count / pageSize);
|
||||||
|
|
||||||
|
return {
|
||||||
|
history: history.rows,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages,
|
||||||
|
totalHistory: history.count
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new HistoryService();
|
18
history/tsconfig.json
Executable file
18
history/tsconfig.json
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2021",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
2
postgres/.gitignore
vendored
Executable file
2
postgres/.gitignore
vendored
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
/pgdata
|
8
postgres/README.md
Executable file
8
postgres/README.md
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
### Postgres
|
||||||
|
*docker commands*
|
||||||
|
|
||||||
|
Start: `sudo docker start postgres`
|
||||||
|
`sudo docker compose up -d`
|
||||||
|
|
||||||
|
Stop: `sudo docker stop postgres`
|
6
postgres/config.env
Executable file
6
postgres/config.env
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
POSTGRES_HOST: postgres
|
||||||
|
POSTGRES_PORT: 5432
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_DB=postgres
|
||||||
|
POSTGRES_PASSWORD=2wroxrnr8fdxicvw2nsd
|
||||||
|
|
21
postgres/docker-compose.yml
Executable file
21
postgres/docker-compose.yml
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
container_name: postgres
|
||||||
|
image: postgres:16.4-alpine3.20
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- proxynet
|
||||||
|
env_file:
|
||||||
|
- ./config.env
|
||||||
|
environment:
|
||||||
|
PG_DATA: /var/lib/postgresql/data
|
||||||
|
volumes:
|
||||||
|
- ./pgdata:/var/lib/postgresql/data
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.tcp.routers.postgresql.rule=HostSNI(`*`)"
|
||||||
|
- "traefik.tcp.services.postgresql.loadbalancer.server.port=5432"
|
||||||
|
- "traefik.tcp.routers.postgresql.entrypoints=postgres"
|
||||||
|
networks:
|
||||||
|
proxynet:
|
||||||
|
external: true
|
2
rabbitmq/.gitignore
vendored
Executable file
2
rabbitmq/.gitignore
vendored
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
/rabbitmq
|
17
rabbitmq/README.md
Executable file
17
rabbitmq/README.md
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
### RabbitMQ
|
||||||
|
|
||||||
|
*WEB GUI*
|
||||||
|
|
||||||
|
Path: `http://localhost:15672/`
|
||||||
|
User: rabbit
|
||||||
|
Password: 5jbya3ptfrezyop6gy8w
|
||||||
|
|
||||||
|
|
||||||
|
*docker commands*
|
||||||
|
|
||||||
|
Start: `sudo docker compose up -d`
|
||||||
|
`sudo docker start rabbitmq`
|
||||||
|
|
||||||
|
Stop: `sudo docker stop rabbitmq`
|
||||||
|
|
3
rabbitmq/config.env
Executable file
3
rabbitmq/config.env
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
RABBITMQ_DEFAULT_VHOST: vrabbit
|
||||||
|
RABBITMQ_DEFAULT_USER: rabbit
|
||||||
|
RABBITMQ_DEFAULT_PASS: 5jbya3ptfrezyop6gy8w
|
23
rabbitmq/docker-compose.yml
Executable file
23
rabbitmq/docker-compose.yml
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
services:
|
||||||
|
rabbitmq:
|
||||||
|
container_name: rabbitmq
|
||||||
|
image: rabbitmq:4.0.3-management-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- proxynet
|
||||||
|
env_file:
|
||||||
|
- ./config.env
|
||||||
|
volumes:
|
||||||
|
- ./rabbitmq:/var/lib/rabbitmq
|
||||||
|
stop_grace_period: 10s
|
||||||
|
ports:
|
||||||
|
- "5672:5672"
|
||||||
|
labels:
|
||||||
|
# WEB GUI
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.tcp.routers.mq.rule=HostSNI(`*`)"
|
||||||
|
- "traefik.tcp.services.mq.loadbalancer.server.port=15672"
|
||||||
|
- "traefik.tcp.routers.mq.entrypoints=rabbitmq-gui"
|
||||||
|
networks:
|
||||||
|
proxynet:
|
||||||
|
external: true
|
26
run.all.sh
Executable file
26
run.all.sh
Executable file
@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
|
||||||
|
cd traefik
|
||||||
|
docker compose up -d
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
cd postgres
|
||||||
|
docker compose up -d
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
cd rabbitmq
|
||||||
|
docker compose up -d
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
cd stocks
|
||||||
|
docker compose up -d
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
cd history
|
||||||
|
docker compose up -d
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
cd users
|
||||||
|
docker compose up -d
|
||||||
|
cd ..
|
5
stocks/.dockerignore
Executable file
5
stocks/.dockerignore
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
/node_modules
|
||||||
|
docker-compose.yml
|
||||||
|
docker-compose.dev.yml
|
||||||
|
run.dev.sh
|
||||||
|
run.prod.sh
|
18
stocks/.gitignore
vendored
Executable file
18
stocks/.gitignore
vendored
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
22
stocks/Dockerfile
Executable file
22
stocks/Dockerfile
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
# develop stage
|
||||||
|
FROM node:22.11.0-alpine3.20 AS develop-stage
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm config set fund false --location=global
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
|
||||||
|
# build stage
|
||||||
|
FROM develop-stage AS build-stage
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
|
||||||
|
# production stage
|
||||||
|
FROM node:22.11.0-alpine3.20 AS production-stage
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build-stage /app/node_modules /app/node_modules
|
||||||
|
COPY --from=build-stage /app/src /app/src
|
||||||
|
COPY package.json ./
|
||||||
|
ENV NODE_PATH=./node_modules
|
||||||
|
CMD ["node", "src/index.js"]
|
||||||
|
EXPOSE 3001
|
115
stocks/README.md
Executable file
115
stocks/README.md
Executable file
@ -0,0 +1,115 @@
|
|||||||
|
|
||||||
|
### Stocks
|
||||||
|
|
||||||
|
Backend stocks v0.1
|
||||||
|
Repository name: `em-shop`
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
*cheat sheet:*
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Запуск сервера в production mode:
|
||||||
|
sudo bash run.prod.sh
|
||||||
|
sudo docker compose up -d --build --force-recreate
|
||||||
|
|
||||||
|
# Запуск сервера в development mode:
|
||||||
|
sudo bash run.dev.sh
|
||||||
|
sudo docker compose -f docker-compose.dev.yml up --build --force-recreate
|
||||||
|
|
||||||
|
# Подключение к консоли контейнера:
|
||||||
|
sudo docker exec -it stocks sh
|
||||||
|
sudo docker exec -it stocks-dev sh
|
||||||
|
|
||||||
|
sudo docker start stocks
|
||||||
|
sudo docker stop stocks
|
||||||
|
|
||||||
|
sudo docker start stocks-dev
|
||||||
|
sudo docker stop stocks-dev
|
||||||
|
|
||||||
|
```
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
#### Общие сведения
|
||||||
|
|
||||||
|
Для успешного запуска и функционирования требуются следующий файл конфигурации:
|
||||||
|
```yml
|
||||||
|
env_file:
|
||||||
|
- ./config.env
|
||||||
|
```
|
||||||
|
|
||||||
|
В `config.env` указываюся переменные:
|
||||||
|
(Используйте эти значения только для разработки)
|
||||||
|
|
||||||
|
```conf
|
||||||
|
PORT=3001
|
||||||
|
URL=/stocks/api
|
||||||
|
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_DB=postgres
|
||||||
|
POSTGRES_PASSWORD=2wroxrnr8fdxicvw2nsd
|
||||||
|
|
||||||
|
RABBITMQ_HOST=rabbitmq
|
||||||
|
RABBITMQ_PORT=5672
|
||||||
|
RABBITMQ_USER=rabbit
|
||||||
|
RABBITMQ_PASSWORD=5jbya3ptfrezyop6gy8w
|
||||||
|
RABBITMQ_VHOST=vrabbit
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Production mode:
|
||||||
|
Режим запуска сервера по умолчанию.
|
||||||
|
Конфигурация описана в `docker-compose.yml`
|
||||||
|
Запуск сервера осуществляется простой командой:
|
||||||
|
|
||||||
|
`docker compose up -d`
|
||||||
|
|
||||||
|
После изменений в образе, запуск производится с пересборкой образа:
|
||||||
|
|
||||||
|
`docker compose up -d --build --force-recreate`
|
||||||
|
|
||||||
|
Для быстрого удобного запуска предусмотрен `скрипт run.prod.sh`, который выполняет команду выше.
|
||||||
|
|
||||||
|
*Пояснения:*
|
||||||
|
Запуск производится по средствам выполнения в образе команды:
|
||||||
|
|
||||||
|
`command: ["node", "src/index.js"]`
|
||||||
|
|
||||||
|
|
||||||
|
#### Development mode:
|
||||||
|
Запуск сервера в режиме разработки. Требуется указать соответствующий образ:
|
||||||
|
|
||||||
|
`docker compose -f docker-compose.dev.yml up -d`
|
||||||
|
|
||||||
|
После изменений в образе, запуск производится с пересборкой образа:
|
||||||
|
|
||||||
|
`docker compose -f docker-compose.dev.yml up -d --build --force-recreate`
|
||||||
|
|
||||||
|
Для быстрого удобного запуска предусмотрен скрипт `run.dev.sh`, который выполняет команду выше.
|
||||||
|
|
||||||
|
*Пояснения:*
|
||||||
|
Запуск производится по средствам выполнения в образе команды:
|
||||||
|
|
||||||
|
`command: sh -c "npm install && npm run start:dev"`
|
||||||
|
|
||||||
|
`start:dev` описана в `package.json` и содержит следующую команду:
|
||||||
|
|
||||||
|
`nodemon ./src/index.js`
|
||||||
|
|
||||||
|
причем, перед запуском сервера выполняется установка `npm install`, что необходимо для корректного прозрачного отображения папки
|
||||||
|
`node_modules` в каталоге, что бы в процессе разработки у IDE был доступ к этой папке для корректного анализа,
|
||||||
|
и подсветки кода в IDE.
|
||||||
|
|
||||||
|
Так же, весь каталог с репозиторием монтируется в режиме записи в как корневой `/app` в Docker контейнере:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
volumes:
|
||||||
|
- .:/app:rw
|
||||||
|
```
|
||||||
|
|
||||||
|
что необходимо для внесения изменений в код без пересборки контейнера.
|
||||||
|
|
||||||
|
В `profuction` режиме сборка осуществляется обычно, `node_modules` и все файлы находятся внутри образа, никаких дополнительных команд, volumes, не используется.
|
||||||
|
|
14
stocks/config.env
Executable file
14
stocks/config.env
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
PORT=3001
|
||||||
|
URL=/stocks/api
|
||||||
|
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_DB=postgres
|
||||||
|
POSTGRES_PASSWORD=2wroxrnr8fdxicvw2nsd
|
||||||
|
|
||||||
|
RABBITMQ_HOST=rabbitmq
|
||||||
|
RABBITMQ_PORT=5672
|
||||||
|
RABBITMQ_USER=rabbit
|
||||||
|
RABBITMQ_PASSWORD=5jbya3ptfrezyop6gy8w
|
||||||
|
RABBITMQ_VHOST=vrabbit
|
30
stocks/docker-compose.dev.yml
Executable file
30
stocks/docker-compose.dev.yml
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
services:
|
||||||
|
stocks-dev:
|
||||||
|
container_name: stocks-dev
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: 'develop-stage'
|
||||||
|
env_file:
|
||||||
|
- ./config.env
|
||||||
|
environment:
|
||||||
|
NODE_ENV: development
|
||||||
|
volumes:
|
||||||
|
- .:/app:rw
|
||||||
|
command: sh -c "npm install && npm run start:dev"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- proxynet
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.apistocks.rule=PathPrefix(`/stocks/api`)"
|
||||||
|
- "traefik.http.routers.apistocks.entrypoints=http"
|
||||||
|
- "traefik.http.routers.apistocks.service=apistocks-service"
|
||||||
|
- "traefik.http.services.apistocks-service.loadbalancer.server.port=3001"
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "1m"
|
||||||
|
networks:
|
||||||
|
proxynet:
|
||||||
|
external: true
|
||||||
|
|
28
stocks/docker-compose.yml
Executable file
28
stocks/docker-compose.yml
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
services:
|
||||||
|
stocks:
|
||||||
|
container_name: stocks
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: 'production-stage'
|
||||||
|
env_file:
|
||||||
|
- ./config.env
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
command: ["node", "src/index.js"]
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- proxynet
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.apistocks.rule=PathPrefix(`/stocks/api`)"
|
||||||
|
- "traefik.http.routers.apistocks.entrypoints=http"
|
||||||
|
- "traefik.http.routers.apistocks.service=apistocks-service"
|
||||||
|
- "traefik.http.services.apistocks-service.loadbalancer.server.port=3001"
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "1m"
|
||||||
|
networks:
|
||||||
|
proxynet:
|
||||||
|
external: true
|
||||||
|
|
1674
stocks/package-lock.json
generated
Normal file
1674
stocks/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
stocks/package.json
Executable file
25
stocks/package.json
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "stocks",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Stocks api server",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node ./src/index.js",
|
||||||
|
"start:dev": "nodemon ./src/index.js"
|
||||||
|
},
|
||||||
|
"author": "Leonid",
|
||||||
|
"license": "Free",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^4.21.1",
|
||||||
|
"express-validator": "^7.2.0",
|
||||||
|
"sequelize": "^6.37.5",
|
||||||
|
"pg": "^8.13.1",
|
||||||
|
"amqplib": "^0.10.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.1.7"
|
||||||
|
}
|
||||||
|
}
|
8
stocks/run.dev.sh
Executable file
8
stocks/run.dev.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ "$(id -u)" != "0" ]; then
|
||||||
|
echo -e "\033[31mThis script requires superuser rights.\033[0m"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo docker compose -f docker-compose.dev.yml up --build --force-recreate
|
8
stocks/run.prod.sh
Executable file
8
stocks/run.prod.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ "$(id -u)" != "0" ]; then
|
||||||
|
echo -e "\033[31mThis script requires superuser rights.\033[0m"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo docker compose up -d --build --force-recreate
|
39
stocks/src/controllers/product-controller.js
Executable file
39
stocks/src/controllers/product-controller.js
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
import productService from '../service/product-service.js';
|
||||||
|
import { validationResult } from 'express-validator';
|
||||||
|
import ApiError from '../exceptions/api-error.js';
|
||||||
|
|
||||||
|
class ProductController {
|
||||||
|
async createProduct(req, res, next) {
|
||||||
|
try {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return next(ApiError.BadRequest('Validation error', errors.array()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { plu, name } = req.body;
|
||||||
|
const productData = await productService.createProduct(plu, name);
|
||||||
|
|
||||||
|
return res.json(productData);
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProducts(req, res, next) {
|
||||||
|
try {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return next(ApiError.BadRequest('Validation error', errors.array()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { plu, name, page, page_size } = req.query;
|
||||||
|
const products = await productService.getProducts(plu, name, page, page_size);
|
||||||
|
return res.json(products);
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ProductController();
|
24
stocks/src/controllers/shop-controller.js
Executable file
24
stocks/src/controllers/shop-controller.js
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
import shopService from '../service/shop-service.js';
|
||||||
|
import { validationResult } from 'express-validator';
|
||||||
|
import ApiError from '../exceptions/api-error.js';
|
||||||
|
|
||||||
|
class ShopController {
|
||||||
|
async createShop(req, res, next) {
|
||||||
|
try {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return next(ApiError.BadRequest('Validation error', errors.array()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name } = req.body;
|
||||||
|
const shopData = await shopService.createShop(name);
|
||||||
|
|
||||||
|
return res.json(shopData);
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ShopController();
|
69
stocks/src/controllers/stocks-controller.js
Executable file
69
stocks/src/controllers/stocks-controller.js
Executable file
@ -0,0 +1,69 @@
|
|||||||
|
|
||||||
|
import stocksService from '../service/stocks-service.js';
|
||||||
|
import { validationResult } from 'express-validator';
|
||||||
|
import ApiError from '../exceptions/api-error.js';
|
||||||
|
|
||||||
|
class StocksController {
|
||||||
|
async createStocks(req, res, next) {
|
||||||
|
try {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return next(ApiError.BadRequest('Validation error', errors.array()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { plu, shop_id, in_stock, in_order } = req.body;
|
||||||
|
const stocksData = await stocksService.createStocks(plu, shop_id, in_stock, in_order);
|
||||||
|
|
||||||
|
return res.json(stocksData);
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStocks(req, res, next) {
|
||||||
|
try {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return next(ApiError.BadRequest('Validation error', errors.array()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { plu, shop_id, shelf_from, shelf_to, order_from, order_to, page, page_size } = req.query;
|
||||||
|
const stocks = await stocksService.getStocks(plu, shop_id, shelf_from, shelf_to, order_from, order_to, page, page_size);
|
||||||
|
return res.json(stocks);
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async increaseStocks(req, res, next) {
|
||||||
|
try {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return next(ApiError.BadRequest('Validation error', errors.array()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, plu, shop_id, increase_shelf, increase_order } = req.body;
|
||||||
|
const increased = await stocksService.changeStocks(true, id, plu, shop_id, increase_shelf, increase_order);
|
||||||
|
return res.json(increased);
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async decreaseStocks(req, res, next) {
|
||||||
|
try {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return next(ApiError.BadRequest('Validation error', errors.array()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, plu, shop_id, decrease_shelf, decrease_order } = req.body;
|
||||||
|
const decreased = await stocksService.changeStocks(false, id, plu, shop_id, decrease_shelf, decrease_order);
|
||||||
|
return res.json(decreased);
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new StocksController();
|
13
stocks/src/db.js
Executable file
13
stocks/src/db.js
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
import { Sequelize } from 'sequelize';
|
||||||
|
|
||||||
|
const sequelize = new Sequelize({
|
||||||
|
dialect: 'postgres',
|
||||||
|
host: process.env.POSTGRES_HOST,
|
||||||
|
port: process.env.POSTGRES_PORT,
|
||||||
|
username: process.env.POSTGRES_USER,
|
||||||
|
database: process.env.POSTGRES_DB,
|
||||||
|
password: process.env.POSTGRES_PASSWORD
|
||||||
|
});
|
||||||
|
|
||||||
|
export default sequelize;
|
21
stocks/src/exceptions/api-error.js
Executable file
21
stocks/src/exceptions/api-error.js
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
class ApiError extends Error {
|
||||||
|
status;
|
||||||
|
errors;
|
||||||
|
|
||||||
|
constructor(status, message, errors = []) {
|
||||||
|
super(message);
|
||||||
|
this.status = status;
|
||||||
|
this.errors = errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
static UnauthorizedError() {
|
||||||
|
return new ApiError(401, 'Пользователь не авторизован')
|
||||||
|
}
|
||||||
|
|
||||||
|
static BadRequest(message, errors = []) {
|
||||||
|
return new ApiError(400, message, errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiError;
|
35
stocks/src/index.js
Executable file
35
stocks/src/index.js
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
// Stocks v0.1
|
||||||
|
|
||||||
|
import http from 'http';
|
||||||
|
import express from 'express';
|
||||||
|
import sequelize from './db.js';
|
||||||
|
import rabbitMqService from './rabbitmq.js';
|
||||||
|
import router from './router/router.js';
|
||||||
|
import errorMiddleware from './middlewares/error-middleware.js';
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
const app = express();
|
||||||
|
const server = http.createServer(app);
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(process.env.URL, router);
|
||||||
|
app.use(errorMiddleware);
|
||||||
|
|
||||||
|
const start = async () => {
|
||||||
|
try {
|
||||||
|
await sequelize.authenticate();
|
||||||
|
console.log('Connection has been established successfully');
|
||||||
|
|
||||||
|
await sequelize.sync().then(() => {
|
||||||
|
console.log('Database and tables created!');
|
||||||
|
});
|
||||||
|
|
||||||
|
rabbitMqService.connectRabbitMq('logs');
|
||||||
|
|
||||||
|
server.listen(PORT, () => console.log(`Server started on PORT = ${PORT}`));
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start();
|
14
stocks/src/middlewares/auth-middleware.js
Executable file
14
stocks/src/middlewares/auth-middleware.js
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
import ApiError from '../exceptions/api-error.js'
|
||||||
|
|
||||||
|
export default function (req, res, next) {
|
||||||
|
try {
|
||||||
|
if (false) {
|
||||||
|
return next(ApiError.UnauthorizedError());
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
} catch (e) {
|
||||||
|
return next(ApiError.UnauthorizedError());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
11
stocks/src/middlewares/error-middleware.js
Executable file
11
stocks/src/middlewares/error-middleware.js
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
import ApiError from '../exceptions/api-error.js';
|
||||||
|
|
||||||
|
export default function (err, req, res, next) {
|
||||||
|
console.log(err);
|
||||||
|
if (err instanceof ApiError) {
|
||||||
|
return res.status(err.status).json({message: err.message, errors: err.errors})
|
||||||
|
}
|
||||||
|
return res.status(500).json({message: 'Ошибка сервера'})
|
||||||
|
};
|
||||||
|
|
21
stocks/src/models/product-model.js
Executable file
21
stocks/src/models/product-model.js
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
import sequelize from '../db.js';
|
||||||
|
import { DataTypes, Model } from 'sequelize';
|
||||||
|
|
||||||
|
class Product extends Model {}
|
||||||
|
|
||||||
|
Product.init({
|
||||||
|
plu: { type: DataTypes.INTEGER, primaryKey: true, allowNull: false },
|
||||||
|
name: { type: DataTypes.STRING, defaultValue: "" },
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'product',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Product;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
20
stocks/src/models/shop-model.js
Executable file
20
stocks/src/models/shop-model.js
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
import sequelize from '../db.js';
|
||||||
|
import { DataTypes, Model } from 'sequelize';
|
||||||
|
|
||||||
|
class Shop extends Model {}
|
||||||
|
|
||||||
|
Shop.init({
|
||||||
|
name: { type: DataTypes.STRING, defaultValue: "" },
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'shop',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Shop;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
28
stocks/src/models/stocks-model.js
Executable file
28
stocks/src/models/stocks-model.js
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
import sequelize from '../db.js';
|
||||||
|
import { DataTypes, Model } from 'sequelize';
|
||||||
|
import ProductModel from './product-model.js';
|
||||||
|
import ShopModel from './shop-model.js';
|
||||||
|
|
||||||
|
class Stocks extends Model {}
|
||||||
|
|
||||||
|
Stocks.init({
|
||||||
|
plu: { type: DataTypes.INTEGER, defaultValue: null },
|
||||||
|
shop_id: { type: DataTypes.INTEGER, defaultValue: null },
|
||||||
|
in_shelf: { type: DataTypes.INTEGER, defaultValue: 0 },
|
||||||
|
in_order: { type: DataTypes.INTEGER, defaultValue: 0 }
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'stocks',
|
||||||
|
});
|
||||||
|
|
||||||
|
Stocks.belongsTo(ShopModel, { as: 'shop', foreignKey: 'shop_id' });
|
||||||
|
Stocks.belongsTo(ProductModel, { as: 'product', foreignKey: 'plu' });
|
||||||
|
|
||||||
|
export default Stocks;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
41
stocks/src/rabbitmq.js
Executable file
41
stocks/src/rabbitmq.js
Executable file
@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
import amqp from "amqplib";
|
||||||
|
|
||||||
|
class RabbitMqService {
|
||||||
|
constructor() {
|
||||||
|
this.channel = null;
|
||||||
|
this.queue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectRabbitMq(queue) {
|
||||||
|
try {
|
||||||
|
const connection = await amqp.connect({
|
||||||
|
protocol: 'amqp',
|
||||||
|
hostname: process.env.RABBITMQ_HOST,
|
||||||
|
port: process.env.RABBITMQ_PORT,
|
||||||
|
username: process.env.RABBITMQ_USER,
|
||||||
|
password: process.env.RABBITMQ_PASSWORD,
|
||||||
|
vhost: process.env.RABBITMQ_VHOST,
|
||||||
|
});
|
||||||
|
this.channel = await connection.createChannel();
|
||||||
|
await this.channel.assertQueue(queue);
|
||||||
|
this.queue = queue;
|
||||||
|
console.log("RabbitMQ connected");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error connecting to RabbitMQ", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getChannel() {
|
||||||
|
return this.channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendToQueue(data) {
|
||||||
|
this.channel.sendToQueue(this.queue, Buffer.from(JSON.stringify(data)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rabbitMqService = new RabbitMqService();
|
||||||
|
|
||||||
|
export default rabbitMqService;
|
69
stocks/src/router/router.js
Executable file
69
stocks/src/router/router.js
Executable file
@ -0,0 +1,69 @@
|
|||||||
|
|
||||||
|
import { Router } from 'express';
|
||||||
|
import productController from '../controllers/product-controller.js';
|
||||||
|
import shopController from '../controllers/shop-controller.js';
|
||||||
|
import stocksController from '../controllers/stocks-controller.js';
|
||||||
|
import authMiddleware from '../middlewares/auth-middleware.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
productDataValidate,
|
||||||
|
shopDataValidate,
|
||||||
|
stocksDataValidate,
|
||||||
|
increaseDataValidate,
|
||||||
|
decreaseDataValidate,
|
||||||
|
getStocksDataValidate,
|
||||||
|
getProductsDataValidate
|
||||||
|
} from './validators.js';
|
||||||
|
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/product/create',
|
||||||
|
authMiddleware,
|
||||||
|
productDataValidate,
|
||||||
|
productController.createProduct
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/shop/create',
|
||||||
|
authMiddleware,
|
||||||
|
shopDataValidate,
|
||||||
|
shopController.createShop
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/stocks/create',
|
||||||
|
authMiddleware,
|
||||||
|
stocksDataValidate,
|
||||||
|
stocksController.createStocks
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
"/stocks/increase",
|
||||||
|
authMiddleware,
|
||||||
|
increaseDataValidate,
|
||||||
|
stocksController.increaseStocks
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
'/stocks/decrease',
|
||||||
|
authMiddleware,
|
||||||
|
decreaseDataValidate,
|
||||||
|
stocksController.decreaseStocks
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/stocks',
|
||||||
|
authMiddleware,
|
||||||
|
getStocksDataValidate,
|
||||||
|
stocksController.getStocks
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/products',
|
||||||
|
authMiddleware,
|
||||||
|
getProductsDataValidate,
|
||||||
|
productController.getProducts
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
134
stocks/src/router/validators.js
Normal file
134
stocks/src/router/validators.js
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
|
||||||
|
import { body, query } from 'express-validator';
|
||||||
|
|
||||||
|
// product/create:
|
||||||
|
export const productDataValidate = [
|
||||||
|
body('plu')
|
||||||
|
.exists({ checkFalsy: true, checkNull: true })
|
||||||
|
.withMessage('поле plu обязательно')
|
||||||
|
.isNumeric()
|
||||||
|
.withMessage('plu должен быть числом')
|
||||||
|
.isLength({ min: 4, max: 5 })
|
||||||
|
.withMessage('plu должен состоять из 4-5 цифр'),
|
||||||
|
body('name')
|
||||||
|
.exists({ checkFalsy: true, checkNull: true })
|
||||||
|
.withMessage('поле name обязательно')
|
||||||
|
.isLength({ min: 3, max: 100 })
|
||||||
|
.withMessage('поле name должно содержать не менее трех и не более 100 символов')
|
||||||
|
];
|
||||||
|
|
||||||
|
// shop/create:
|
||||||
|
export const shopDataValidate = [
|
||||||
|
body('name')
|
||||||
|
.exists({ checkFalsy: true, checkNull: true })
|
||||||
|
.withMessage('поле name обязательно')
|
||||||
|
.isLength({ min: 3, max: 100 })
|
||||||
|
.withMessage('поле name должно содержать не менее трех и не более 100 символов')
|
||||||
|
];
|
||||||
|
|
||||||
|
// stocks/create:
|
||||||
|
export const stocksDataValidate = [
|
||||||
|
body('plu')
|
||||||
|
.exists({ checkFalsy: true, checkNull: true })
|
||||||
|
.withMessage('поле plu обязательно')
|
||||||
|
.isNumeric()
|
||||||
|
.withMessage('plu должен быть числом')
|
||||||
|
.isLength({ min: 4, max: 5 })
|
||||||
|
.withMessage('plu должен состоять из 4-5 цифр'),
|
||||||
|
body('shop_id')
|
||||||
|
.exists({ checkFalsy: true, checkNull: true })
|
||||||
|
.withMessage('поле shop_id обязательно')
|
||||||
|
.isNumeric()
|
||||||
|
.withMessage('поле shop_id должно быть числом'),
|
||||||
|
body('in_stock')
|
||||||
|
.default(0)
|
||||||
|
.exists({ checkFalsy: true, checkNull: true })
|
||||||
|
.isNumeric()
|
||||||
|
.withMessage('поле in_stock должно быть числом'),
|
||||||
|
body('in_order')
|
||||||
|
.default(0)
|
||||||
|
.exists({ checkFalsy: true, checkNull: true })
|
||||||
|
.isNumeric()
|
||||||
|
.withMessage('поле in_order должно быть числом'),
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
// stocks/increase:
|
||||||
|
export const increaseDataValidate = [
|
||||||
|
body('id')
|
||||||
|
.toFloat()
|
||||||
|
.default(null),
|
||||||
|
body('plu')
|
||||||
|
.toFloat()
|
||||||
|
.default(null),
|
||||||
|
body('shop_id')
|
||||||
|
.toFloat()
|
||||||
|
.default(null),
|
||||||
|
body('increase_shelf')
|
||||||
|
.toFloat()
|
||||||
|
.default(0),
|
||||||
|
body('increase_order')
|
||||||
|
.toFloat()
|
||||||
|
.default(0),
|
||||||
|
];
|
||||||
|
|
||||||
|
// stocks/decrease:
|
||||||
|
export const decreaseDataValidate = [
|
||||||
|
body('id')
|
||||||
|
.toFloat()
|
||||||
|
.default(null),
|
||||||
|
body('plu')
|
||||||
|
.toFloat()
|
||||||
|
.default(null),
|
||||||
|
body('shop_id')
|
||||||
|
.toFloat()
|
||||||
|
.default(null),
|
||||||
|
body('decrease_shelf')
|
||||||
|
.toFloat()
|
||||||
|
.default(0),
|
||||||
|
body('decrease_order')
|
||||||
|
.toFloat()
|
||||||
|
.default(0),
|
||||||
|
];
|
||||||
|
|
||||||
|
// stocks:
|
||||||
|
export const getStocksDataValidate = [
|
||||||
|
query('plu')
|
||||||
|
.toFloat()
|
||||||
|
.default(null),
|
||||||
|
query('shop_id')
|
||||||
|
.toFloat()
|
||||||
|
.default(null),
|
||||||
|
query('shelf_from')
|
||||||
|
.toFloat()
|
||||||
|
.default(null),
|
||||||
|
query('shelf_to')
|
||||||
|
.toFloat()
|
||||||
|
.default(null),
|
||||||
|
query('order_from')
|
||||||
|
.toFloat()
|
||||||
|
.default(null),
|
||||||
|
query('order_to')
|
||||||
|
.toFloat()
|
||||||
|
.default(null),
|
||||||
|
query('page')
|
||||||
|
.toFloat()
|
||||||
|
.default(1),
|
||||||
|
query('page_size')
|
||||||
|
.toFloat()
|
||||||
|
.default(10),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getProductsDataValidate = [
|
||||||
|
query('plu')
|
||||||
|
.toFloat()
|
||||||
|
.default(null),
|
||||||
|
query('name')
|
||||||
|
.default(null),
|
||||||
|
query('page')
|
||||||
|
.toFloat()
|
||||||
|
.default(1),
|
||||||
|
query('page_size')
|
||||||
|
.toFloat()
|
||||||
|
.default(10),
|
||||||
|
];
|
61
stocks/src/service/product-service.js
Executable file
61
stocks/src/service/product-service.js
Executable file
@ -0,0 +1,61 @@
|
|||||||
|
|
||||||
|
import ProductModel from '../models/product-model.js';
|
||||||
|
import ApiError from '../exceptions/api-error.js';
|
||||||
|
import rabbitMqService from '../rabbitmq.js';
|
||||||
|
import { Op } from 'sequelize';
|
||||||
|
|
||||||
|
class ProductService {
|
||||||
|
async createProduct(plu, name) {
|
||||||
|
const existingProduct = await ProductModel.findOne({ where: { plu }});
|
||||||
|
if (existingProduct) {
|
||||||
|
throw ApiError.BadRequest('Продукт с таким PLU уже существует');
|
||||||
|
}
|
||||||
|
|
||||||
|
const product = await ProductModel.create({ plu, name });
|
||||||
|
|
||||||
|
rabbitMqService.sendToQueue({
|
||||||
|
action: 'createProduct',
|
||||||
|
shop_id: null,
|
||||||
|
plu: product.plu,
|
||||||
|
oldData: null,
|
||||||
|
newData: product
|
||||||
|
});
|
||||||
|
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProducts(
|
||||||
|
plu = null,
|
||||||
|
name = null,
|
||||||
|
page = 1,
|
||||||
|
pageSize = 10
|
||||||
|
) {
|
||||||
|
const offset = (page - 1) * pageSize;
|
||||||
|
const limit = pageSize;
|
||||||
|
const where = {};
|
||||||
|
|
||||||
|
if(plu) where.plu = plu;
|
||||||
|
if(name) where.name = {[Op.iLike]: `%${name}%`};
|
||||||
|
|
||||||
|
const products = await ProductModel.findAndCountAll({ limit, offset, where });
|
||||||
|
const totalPages = Math.ceil(products.count / pageSize);
|
||||||
|
|
||||||
|
rabbitMqService.sendToQueue({
|
||||||
|
action: 'getProducts',
|
||||||
|
shop_id: null,
|
||||||
|
plu,
|
||||||
|
oldData: null,
|
||||||
|
newData: null
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
products: products.rows,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages,
|
||||||
|
totalProducts: products.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ProductService();
|
27
stocks/src/service/shop-service.js
Executable file
27
stocks/src/service/shop-service.js
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
import ApiError from '../exceptions/api-error.js';
|
||||||
|
import ShopModel from '../models/shop-model.js';
|
||||||
|
import rabbitMqService from '../rabbitmq.js';
|
||||||
|
|
||||||
|
class ShopService {
|
||||||
|
async createShop(name) {
|
||||||
|
const existingShop = await ShopModel.findOne({ where: { name }});
|
||||||
|
if (existingShop) {
|
||||||
|
throw ApiError.BadRequest('Магазин с таким названием уже существует');
|
||||||
|
}
|
||||||
|
|
||||||
|
const shop = await ShopModel.create({ name });
|
||||||
|
|
||||||
|
rabbitMqService.sendToQueue({
|
||||||
|
action: 'createShop',
|
||||||
|
shop_id: shop.id,
|
||||||
|
plu: null,
|
||||||
|
oldData: null,
|
||||||
|
newData: shop
|
||||||
|
});
|
||||||
|
|
||||||
|
return shop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ShopService();
|
147
stocks/src/service/stocks-service.js
Executable file
147
stocks/src/service/stocks-service.js
Executable file
@ -0,0 +1,147 @@
|
|||||||
|
|
||||||
|
import ApiError from '../exceptions/api-error.js';
|
||||||
|
import StocksModel from '../models/stocks-model.js';
|
||||||
|
import ShopModel from '../models/shop-model.js';
|
||||||
|
import ProductModel from '../models/product-model.js';
|
||||||
|
import rabbitMqService from '../rabbitmq.js';
|
||||||
|
import { Op } from 'sequelize';
|
||||||
|
|
||||||
|
|
||||||
|
class StocksService {
|
||||||
|
async createStocks(plu, shop_id, in_shelf, in_order) {
|
||||||
|
const existingStock = await StocksModel.findOne({ where: { plu, shop_id } });
|
||||||
|
if (existingStock) {
|
||||||
|
throw ApiError.BadRequest('Запись об остатке с таким товаром и магазином уже существует в базе данных');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingPlu = await ProductModel.findOne({ where: { plu } });
|
||||||
|
if (!existingPlu) {
|
||||||
|
throw ApiError.BadRequest(`Товар с PLU ${plu} отсутствует в базе`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingShop = await ShopModel.findOne({ where: { id: shop_id } });
|
||||||
|
if (!existingShop) {
|
||||||
|
throw ApiError.BadRequest(`Магазин с кодом ${shop_id} отсутствует в базе`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stocks = await StocksModel.create({ plu, shop_id, in_shelf, in_order });
|
||||||
|
|
||||||
|
rabbitMqService.sendToQueue({
|
||||||
|
action: 'createStocks',
|
||||||
|
shop_id: stocks.shop_id,
|
||||||
|
plu: stocks.plu,
|
||||||
|
oldData: null,
|
||||||
|
newData: stocks
|
||||||
|
});
|
||||||
|
|
||||||
|
return stocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStocks(
|
||||||
|
plu = null,
|
||||||
|
shopId = null,
|
||||||
|
shelfFrom = null,
|
||||||
|
shelfTo = null,
|
||||||
|
orderFrom = null,
|
||||||
|
orderTo = null,
|
||||||
|
page = 1,
|
||||||
|
pageSize = 10
|
||||||
|
) {
|
||||||
|
const offset = (page - 1) * pageSize;
|
||||||
|
const limit = pageSize;
|
||||||
|
|
||||||
|
const where = {};
|
||||||
|
|
||||||
|
if(plu) where.plu = plu;
|
||||||
|
if(shopId) where.shop_id = shopId;
|
||||||
|
|
||||||
|
if(shelfFrom) { where.in_shelf = {}; where.in_shelf[Op.gt] = shelfFrom; };
|
||||||
|
if(shelfTo) { where.in_shelf = {}; where.in_shelf[Op.lt] = shelfTo; };
|
||||||
|
if(orderFrom) { where.in_order = {}; where.in_order[Op.gt] = orderFrom; };
|
||||||
|
if(orderTo) { where.in_order = {}; where.in_order[Op.lt] = orderTo; };
|
||||||
|
|
||||||
|
const stocks = await StocksModel.findAndCountAll({
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
where,
|
||||||
|
include: [
|
||||||
|
{ model: ShopModel, as: 'shop', attributes: ['name'] },
|
||||||
|
{ model: ProductModel, as: 'product', attributes: ['name'] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(stocks.count / pageSize);
|
||||||
|
|
||||||
|
rabbitMqService.sendToQueue({
|
||||||
|
action: 'getStocks',
|
||||||
|
shop_id: shopId,
|
||||||
|
plu,
|
||||||
|
oldData: null,
|
||||||
|
newData: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedStocks = stocks.rows.map(stock => ({
|
||||||
|
id: stock.id,
|
||||||
|
plu: stock.plu,
|
||||||
|
shopId: stock.shop_id,
|
||||||
|
inShelf: stock.in_shelf,
|
||||||
|
inOrder: stock.in_order,
|
||||||
|
createdAt: stock.createdAt,
|
||||||
|
updatedAt: stock.updatedAt,
|
||||||
|
shopName: stock.shop.name,
|
||||||
|
productName: stock.product.name
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
stocks: formattedStocks,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages,
|
||||||
|
totalStocks: stocks.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async changeStocks(isIncrease, id, plu, shop_id, change_shelf, change_order) {
|
||||||
|
if(!isIncrease) {
|
||||||
|
change_shelf *= -1;
|
||||||
|
change_order *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stock;
|
||||||
|
|
||||||
|
if(id) {
|
||||||
|
stock = await StocksModel.findOne({ where: { id }});
|
||||||
|
if (!stock) {
|
||||||
|
throw ApiError.BadRequest('Остаток с таким id не найден');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(plu && shop_id) {
|
||||||
|
stock = await StocksModel.findOne({ where: { plu, shop_id }});
|
||||||
|
if (!stock) {
|
||||||
|
throw ApiError.BadRequest(`Остаток с PLU ${plu} в магазине с кодом ${shop_id} не найден`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!stock) throw ApiError.BadRequest('Неверный запрос');
|
||||||
|
|
||||||
|
const oldStock = { ...stock.dataValues };
|
||||||
|
stock.in_shelf = stock.in_shelf + change_shelf;
|
||||||
|
stock.in_order = stock.in_order + change_order;
|
||||||
|
|
||||||
|
const newStock = await stock.save();
|
||||||
|
|
||||||
|
rabbitMqService.sendToQueue({
|
||||||
|
action: isIncrease ? 'increaseStocks' : 'decreaseStocks',
|
||||||
|
shop_id: stock.shop_id,
|
||||||
|
plu: stock.plu,
|
||||||
|
oldData: oldStock,
|
||||||
|
newData: newStock
|
||||||
|
});
|
||||||
|
|
||||||
|
return newStock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new StocksService();
|
13
stop.all.sh
Executable file
13
stop.all.sh
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
docker stop traefik
|
||||||
|
|
||||||
|
docker stop postgres
|
||||||
|
|
||||||
|
docker stop rabbitmq
|
||||||
|
|
||||||
|
docker stop stocks
|
||||||
|
|
||||||
|
docker stop history
|
||||||
|
|
||||||
|
docker stop users
|
14
traefik/README.md
Executable file
14
traefik/README.md
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
### Traefik
|
||||||
|
|
||||||
|
Path: `http://localhost/dashboard/`
|
||||||
|
User: `admin`
|
||||||
|
Password: `admin`
|
||||||
|
|
||||||
|
|
||||||
|
*docker commands*
|
||||||
|
|
||||||
|
Start: `sudo docker compose -f /data/traefik/docker-compose.yml up -d`
|
||||||
|
`sudo docker start traefik`
|
||||||
|
|
||||||
|
Stop: `sudo docker stop traefik`
|
38
traefik/docker-compose.yml
Executable file
38
traefik/docker-compose.yml
Executable file
@ -0,0 +1,38 @@
|
|||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
container_name: traefik
|
||||||
|
image: traefik:v3.2.0
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- "--providers.docker.network=proxynet"
|
||||||
|
- "--api.insecure=true"
|
||||||
|
- "--api.dashboard=true"
|
||||||
|
- "--providers.docker"
|
||||||
|
- "--log=true"
|
||||||
|
- "--log.level=DEBUG"
|
||||||
|
- "--providers.docker.exposedByDefault=false"
|
||||||
|
# Entrypoints:
|
||||||
|
- "--entrypoints.http.address=:80"
|
||||||
|
- "--entrypoints.postgres.address=:5432"
|
||||||
|
- "--entrypoints.rabbitmq-gui.address=:15672"
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.dashboard.rule=PathPrefix(`/api`) || PathPrefix(`/dashboard`)"
|
||||||
|
- "traefik.http.routers.dashboard.service=api@internal"
|
||||||
|
- "traefik.http.routers.dashboard.middlewares=auth"
|
||||||
|
- "traefik.http.middlewares.auth.basicauth.usersfile=/httpauth/usersfile.htpasswd"
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "5432:5432"
|
||||||
|
- "15672:15672"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./httpauth:/httpauth
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "1m"
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: proxynet
|
||||||
|
external: true
|
1
traefik/httpauth/usersfile.htpasswd
Executable file
1
traefik/httpauth/usersfile.htpasswd
Executable file
@ -0,0 +1 @@
|
|||||||
|
admin:$2y$10$bPwS1U.tBzICOvX2uhG9cOY4nNA0NvoYqTnCD1Wr5MTcwWupfBTIG
|
25
users/.eslintrc.js
Normal file
25
users/.eslintrc.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
};
|
400
users/.gitignore
vendored
Normal file
400
users/.gitignore
vendored
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### JetBrains template
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff:
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/dictionaries
|
||||||
|
|
||||||
|
# Sensitive or high-churn files:
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.xml
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
|
||||||
|
# Gradle:
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-debug/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin:
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
## File-based project format:
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
## Plugin-specific files:
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
### VisualStudio template
|
||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
##
|
||||||
|
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Ll]og/
|
||||||
|
|
||||||
|
# Visual Studio 2015 cache/options directory
|
||||||
|
.vs/
|
||||||
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
|
#wwwroot/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUNIT
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
# Benchmark Results
|
||||||
|
BenchmarkDotNet.Artifacts/
|
||||||
|
|
||||||
|
# .NET Core
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
**/Properties/launchSettings.json
|
||||||
|
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_i.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
*.VC.db
|
||||||
|
*.VC.VC.opendb
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# Visual Studio Trace Files
|
||||||
|
*.e2e
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# JustCode is a .NET coding add-in
|
||||||
|
.JustCode
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# AxoCover is a Code Coverage Tool
|
||||||
|
.axoCover/*
|
||||||
|
!.axoCover/settings.json
|
||||||
|
|
||||||
|
# Visual Studio code coverage results
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
|
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||||
|
# in these scripts will be unencrypted
|
||||||
|
PublishScripts/
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# The packages folder can be ignored because of Package Restore
|
||||||
|
**/[Pp]ackages/*
|
||||||
|
# except build/, which is used as an MSBuild target.
|
||||||
|
!**/[Pp]ackages/build/
|
||||||
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
#!**/[Pp]ackages/repositories.config
|
||||||
|
# NuGet v3's project.json files produces more ignorable files
|
||||||
|
*.nuget.props
|
||||||
|
*.nuget.targets
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Windows Store app package directories and files
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
Package.StoreAssociation.xml
|
||||||
|
_pkginfo.txt
|
||||||
|
*.appx
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.jfm
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||||
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
|
#bower_components/
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
*.ndf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Typescript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||||
|
*.vbw
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
paket-files/
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
|
|
||||||
|
# JetBrains Rider
|
||||||
|
.idea/
|
||||||
|
*.sln.iml
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# CodeRush
|
||||||
|
.cr/
|
||||||
|
|
||||||
|
# Python Tools for Visual Studio (PTVS)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Cake - Uncomment if you are using it
|
||||||
|
# tools/**
|
||||||
|
# !tools/packages.config
|
||||||
|
|
||||||
|
# Tabs Studio
|
||||||
|
*.tss
|
||||||
|
|
||||||
|
# Telerik's JustMock configuration file
|
||||||
|
*.jmconfig
|
||||||
|
|
||||||
|
# BizTalk build output
|
||||||
|
*.btp.cs
|
||||||
|
*.btm.cs
|
||||||
|
*.odx.cs
|
||||||
|
*.xsd.cs
|
||||||
|
|
||||||
|
# OpenCover UI analysis results
|
||||||
|
OpenCover/
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
### macOS template
|
||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
=======
|
||||||
|
# Local
|
||||||
|
.env
|
||||||
|
dist
|
||||||
|
.webpack
|
||||||
|
.serverless/**/*.zip
|
4
users/.prettierrc
Normal file
4
users/.prettierrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
22
users/Dockerfile
Executable file
22
users/Dockerfile
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
# develop stage
|
||||||
|
FROM node:22.11.0-alpine3.20 AS develop-stage
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm config set fund false --location=global
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
|
||||||
|
# build stage
|
||||||
|
FROM develop-stage AS build-stage
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
|
# production stage
|
||||||
|
FROM node:22.11.0-alpine3.20 AS production-stage
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build-stage /app/node_modules /app/node_modules
|
||||||
|
COPY --from=build-stage /app/dist /app/dist
|
||||||
|
COPY package.json ./
|
||||||
|
CMD ["node", "dist/main.js"]
|
||||||
|
EXPOSE 3003
|
81
users/README.md
Normal file
81
users/README.md
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
|
||||||
|
### Users
|
||||||
|
|
||||||
|
Backend users v0.1
|
||||||
|
Repository name: `em-shop`
|
||||||
|
|
||||||
|
Swagger: `http://localhost/users/api/docs/`
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
*cheat sheet:*
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Запуск сервера в production mode:
|
||||||
|
sudo bash run.prod.sh
|
||||||
|
sudo docker compose up -d --build --force-recreate
|
||||||
|
|
||||||
|
# Запуск сервера в development mode:
|
||||||
|
sudo bash run.dev.sh
|
||||||
|
sudo docker compose -f docker-compose.dev.yml up --build --force-recreate
|
||||||
|
|
||||||
|
# Подключение к консоли контейнера:
|
||||||
|
sudo docker exec -it users sh
|
||||||
|
sudo docker exec -it users-dev sh
|
||||||
|
|
||||||
|
sudo docker start users
|
||||||
|
sudo docker stop users
|
||||||
|
|
||||||
|
sudo docker start users-dev
|
||||||
|
sudo docker stop users-dev
|
||||||
|
|
||||||
|
```
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
#### Общие сведения
|
||||||
|
|
||||||
|
Для успешного запуска и функционирования требуются следующий файл конфигурации:
|
||||||
|
```yml
|
||||||
|
env_file:
|
||||||
|
- ./config.env
|
||||||
|
```
|
||||||
|
|
||||||
|
В `config.env` указываюся переменные:
|
||||||
|
(Используйте эти значения только для разработки)
|
||||||
|
|
||||||
|
```conf
|
||||||
|
PORT=3003
|
||||||
|
URL=/users/api
|
||||||
|
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_DB=postgres
|
||||||
|
POSTGRES_PASSWORD=2wroxrnr8fdxicvw2nsd
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Production mode:
|
||||||
|
Режим запуска сервера по умолчанию.
|
||||||
|
Конфигурация описана в `docker-compose.yml`
|
||||||
|
Запуск сервера осуществляется простой командой:
|
||||||
|
|
||||||
|
`docker compose up -d`
|
||||||
|
|
||||||
|
После изменений в образе, запуск производится с пересборкой образа:
|
||||||
|
|
||||||
|
`docker compose up -d --build --force-recreate`
|
||||||
|
|
||||||
|
Для быстрого удобного запуска предусмотрен `скрипт run.prod.sh`, который выполняет команду выше.
|
||||||
|
|
||||||
|
|
||||||
|
#### Development mode:
|
||||||
|
Запуск сервера в режиме разработки. Требуется указать соответствующий образ:
|
||||||
|
|
||||||
|
`docker compose -f docker-compose.dev.yml up -d`
|
||||||
|
|
||||||
|
После изменений в образе, запуск производится с пересборкой образа:
|
||||||
|
|
||||||
|
`docker compose -f docker-compose.dev.yml up -d --build --force-recreate`
|
||||||
|
|
||||||
|
Для быстрого удобного запуска предусмотрен скрипт `run.dev.sh`, который выполняет команду выше.
|
8
users/config.env
Executable file
8
users/config.env
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
PORT=3003
|
||||||
|
URL=/users/api
|
||||||
|
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_DB=postgres
|
||||||
|
POSTGRES_PASSWORD=2wroxrnr8fdxicvw2nsd
|
23
users/config/config.js
Executable file
23
users/config/config.js
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
module.exports = {
|
||||||
|
development: {
|
||||||
|
username: process.env.POSTGRES_USER,
|
||||||
|
password: process.env.POSTGRES_PASSWORD,
|
||||||
|
database: process.env.POSTGRES_DB,
|
||||||
|
host: process.env.POSTGRES_HOST,
|
||||||
|
dialect: "postgres"
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
username: process.env.POSTGRES_USER,
|
||||||
|
password: process.env.POSTGRES_PASSWORD,
|
||||||
|
database: process.env.POSTGRES_DB,
|
||||||
|
host: process.env.POSTGRES_HOST,
|
||||||
|
dialect: "postgres"
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
username: process.env.POSTGRES_USER,
|
||||||
|
password: process.env.POSTGRES_PASSWORD,
|
||||||
|
database: process.env.POSTGRES_DB,
|
||||||
|
host: process.env.POSTGRES_HOST,
|
||||||
|
dialect: "postgres"
|
||||||
|
}
|
||||||
|
};
|
30
users/docker-compose.dev.yml
Executable file
30
users/docker-compose.dev.yml
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
services:
|
||||||
|
users-dev:
|
||||||
|
container_name: users-dev
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: 'develop-stage'
|
||||||
|
env_file:
|
||||||
|
- ./config.env
|
||||||
|
environment:
|
||||||
|
NODE_ENV: development
|
||||||
|
volumes:
|
||||||
|
- .:/app:rw
|
||||||
|
command: sh -c "npm install && npm run start:dev"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- proxynet
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.users.rule=PathPrefix(`/users/api`)"
|
||||||
|
- "traefik.http.routers.users.entrypoints=http"
|
||||||
|
- "traefik.http.routers.users.service=users-service"
|
||||||
|
- "traefik.http.services.users-service.loadbalancer.server.port=3003"
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "1m"
|
||||||
|
networks:
|
||||||
|
proxynet:
|
||||||
|
external: true
|
||||||
|
|
28
users/docker-compose.yml
Executable file
28
users/docker-compose.yml
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
services:
|
||||||
|
users:
|
||||||
|
container_name: users
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: 'production-stage'
|
||||||
|
env_file:
|
||||||
|
- ./config.env
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
command: ["node", "dist/main.js"]
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- proxynet
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.users.rule=PathPrefix(`/users/api`)"
|
||||||
|
- "traefik.http.routers.users.entrypoints=http"
|
||||||
|
- "traefik.http.routers.users.service=users-service"
|
||||||
|
- "traefik.http.services.users-service.loadbalancer.server.port=3003"
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "1m"
|
||||||
|
networks:
|
||||||
|
proxynet:
|
||||||
|
external: true
|
||||||
|
|
8
users/migrate.dev.sh
Executable file
8
users/migrate.dev.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if [ "$(id -u)" != "0" ]; then
|
||||||
|
echo -e "\033[31mThis script requires superuser rights.\033[0m"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo docker exec -it users-dev npx sequelize db:migrate
|
8
users/migrate.prod.sh
Executable file
8
users/migrate.prod.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if [ "$(id -u)" != "0" ]; then
|
||||||
|
echo -e "\033[31mThis script requires superuser rights.\033[0m"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo docker exec -it users npx sequelize db:migrate
|
33
users/migrations/20241120031944-add-users.js
Normal file
33
users/migrations/20241120031944-add-users.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface) => {
|
||||||
|
const count = 1000000;
|
||||||
|
const chunkSize = 10000;
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i += chunkSize) {
|
||||||
|
const users = Array.from(
|
||||||
|
{ length: Math.min(chunkSize, count - i) },
|
||||||
|
() => ({
|
||||||
|
firstName:
|
||||||
|
'Firstname_' +
|
||||||
|
(Math.floor(Math.random() * 9_000_000_000) + 1_000_000_000),
|
||||||
|
lastName:
|
||||||
|
'Lastname_' +
|
||||||
|
(Math.floor(Math.random() * 9_000_000_000) + 1_000_000_000),
|
||||||
|
birthday: '2006-11-19 19:00:00.000 +00:00',
|
||||||
|
sex: Math.floor(Math.random() * 2) + 1,
|
||||||
|
problems: Math.random() < 0.5,
|
||||||
|
createdAt: '2006-11-19 19:00:00.000 +00:00',
|
||||||
|
updatedAt: '2006-11-19 19:00:00.000 +00:00',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('user', users, {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface) => {
|
||||||
|
await queryInterface.bulkDelete('user', null, {});
|
||||||
|
},
|
||||||
|
};
|
8
users/nest-cli.json
Normal file
8
users/nest-cli.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
10601
users/package-lock.json
generated
Normal file
10601
users/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
84
users/package.json
Normal file
84
users/package.json
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"name": "nest-typescript-starter",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Nest TypeScript starter repository",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/jest/bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"npm": ">=8.0.0",
|
||||||
|
"node": ">=16.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^10.3.2",
|
||||||
|
"@nestjs/core": "^10.4.4",
|
||||||
|
"@nestjs/platform-express": "^10.4.4",
|
||||||
|
"reflect-metadata": "^0.2.1",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"@nestjs/sequelize": "^10.0.1",
|
||||||
|
"sequelize": "^6.37.5",
|
||||||
|
"sequelize-typescript": "^2.1.6",
|
||||||
|
"pg": "^8.13.1",
|
||||||
|
"@nestjs/swagger": "^8.0.7",
|
||||||
|
"swagger-ui-express": "^5.0.1",
|
||||||
|
"class-validator": "^0.14.1",
|
||||||
|
"class-transformer": "^0.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^10.4.5",
|
||||||
|
"@nestjs/schematics": "^10.1.0",
|
||||||
|
"@nestjs/testing": "^10.3.2",
|
||||||
|
"@swc/cli": "^0.3.9",
|
||||||
|
"@swc/core": "^1.4.0",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/jest": "^29.5.12",
|
||||||
|
"@types/node": "^20.11.16",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^6.3.4",
|
||||||
|
"ts-jest": "^29.1.2",
|
||||||
|
"ts-loader": "^9.5.1",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"@types/sequelize": "^4.28.20",
|
||||||
|
"sequelize-cli": "^6.6.2"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
8
users/run.dev.sh
Executable file
8
users/run.dev.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ "$(id -u)" != "0" ]; then
|
||||||
|
echo -e "\033[31mThis script requires superuser rights.\033[0m"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo docker compose -f docker-compose.dev.yml up --build --force-recreate
|
8
users/run.prod.sh
Executable file
8
users/run.prod.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ "$(id -u)" != "0" ]; then
|
||||||
|
echo -e "\033[31mThis script requires superuser rights.\033[0m"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo docker compose up -d --build --force-recreate
|
21
users/src/app.controller.spec.ts
Normal file
21
users/src/app.controller.spec.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
describe('AppController', () => {
|
||||||
|
let app: TestingModule;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await Test.createTestingModule({
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
}).compile();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getHello', () => {
|
||||||
|
it('should return "Index Page!"', () => {
|
||||||
|
const appController = app.get(AppController);
|
||||||
|
expect(appController.getHello()).toBe('Index Page');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
12
users/src/app.controller.ts
Normal file
12
users/src/app.controller.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
getHello(): string {
|
||||||
|
return this.appService.getHello();
|
||||||
|
}
|
||||||
|
}
|
26
users/src/app.module.ts
Normal file
26
users/src/app.module.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
import { SequelizeModule } from '@nestjs/sequelize';
|
||||||
|
import { UsersModule } from './users/user.module';
|
||||||
|
import { User } from './users/user.model';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
SequelizeModule.forRoot({
|
||||||
|
dialect: 'postgres',
|
||||||
|
host: process.env.POSTGRES_HOST,
|
||||||
|
port: +process.env.POSTGRES_PORT,
|
||||||
|
username: process.env.POSTGRES_USER,
|
||||||
|
password: process.env.POSTGRES_PASSWORD,
|
||||||
|
database: process.env.POSTGRES_DB,
|
||||||
|
synchronize: true,
|
||||||
|
autoLoadModels: true,
|
||||||
|
models: [User],
|
||||||
|
}),
|
||||||
|
UsersModule,
|
||||||
|
],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
8
users/src/app.service.ts
Normal file
8
users/src/app.service.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
getHello(): string {
|
||||||
|
return 'Index Page';
|
||||||
|
}
|
||||||
|
}
|
24
users/src/main.ts
Normal file
24
users/src/main.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
app.setGlobalPrefix(process.env.URL);
|
||||||
|
|
||||||
|
const config = new DocumentBuilder()
|
||||||
|
.setTitle('Users')
|
||||||
|
.setDescription('Documentation REST API')
|
||||||
|
.setVersion('0.0.1')
|
||||||
|
.addTag('em-shop')
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
|
SwaggerModule.setup('/users/api/docs', app, document);
|
||||||
|
|
||||||
|
app.useGlobalPipes(new ValidationPipe({ transform: true }));
|
||||||
|
await app.listen(PORT, () => console.log(`Server port: ${PORT}`));
|
||||||
|
}
|
||||||
|
bootstrap();
|
40
users/src/users/dto/create.user.dto.ts
Normal file
40
users/src/users/dto/create.user.dto.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
Length,
|
||||||
|
IsNumber,
|
||||||
|
IsBoolean,
|
||||||
|
IsNotEmpty,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateUserDto {
|
||||||
|
@ApiProperty({ example: 'Alice', description: 'First Name' })
|
||||||
|
@IsNotEmpty({ message: 'Укажите имя' })
|
||||||
|
@IsString({ message: 'Имя должно быть строкой' })
|
||||||
|
@Length(3, 20, { message: 'Имя не меньше 3 и не больше 20 символов' })
|
||||||
|
readonly firstName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Smith', description: 'Last Name' })
|
||||||
|
@IsNotEmpty({ message: 'Укажите фамилию' })
|
||||||
|
@IsString({ message: 'Фамилия должна быть строкой' })
|
||||||
|
@Length(3, 20, { message: 'Фамилия не меньше 3 и не больше 20 символов' })
|
||||||
|
readonly lastName: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: '2006-11-19 19:00:00.000 +00:00',
|
||||||
|
description: 'Birthday',
|
||||||
|
})
|
||||||
|
@IsNotEmpty({ message: 'Укажите дату рождения' })
|
||||||
|
@IsString({ message: 'Дата рождения должна быть строкой' })
|
||||||
|
@Length(0, 30, { message: 'Дата рождения не более 20 символов' })
|
||||||
|
readonly birthday: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 1, description: 'Sex' })
|
||||||
|
@IsNotEmpty({ message: 'Укажите пол' })
|
||||||
|
@IsNumber({}, { message: 'Пол должен быть числом' })
|
||||||
|
readonly sex: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: true, description: 'Problems' })
|
||||||
|
@IsBoolean({ message: 'Проблема должна быть булевым значением' })
|
||||||
|
readonly problems: boolean;
|
||||||
|
}
|
17
users/src/users/dto/pagination.dto.ts
Normal file
17
users/src/users/dto/pagination.dto.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNumber, IsOptional } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
export class PaginationDto {
|
||||||
|
@ApiProperty({ example: 1, description: 'Номер страницы' })
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber({}, { message: 'Номер страницы должен быть числом' })
|
||||||
|
readonly page?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 10, description: 'Размер страницы' })
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber({}, { message: 'Размер страницы должен быть числом' })
|
||||||
|
readonly pageSize?: number;
|
||||||
|
}
|
77
users/src/users/user.controller.ts
Executable file
77
users/src/users/user.controller.ts
Executable file
@ -0,0 +1,77 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Patch,
|
||||||
|
Delete,
|
||||||
|
Body,
|
||||||
|
Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { UsersService } from './user.service';
|
||||||
|
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { CreateUserDto } from './dto/create.user.dto';
|
||||||
|
import { PaginationDto } from './dto/pagination.dto';
|
||||||
|
import { User } from './user.model';
|
||||||
|
|
||||||
|
@ApiTags('Пользователи')
|
||||||
|
@Controller('users')
|
||||||
|
export class UsersController {
|
||||||
|
constructor(private readonly usersService: UsersService) {}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Создание нового пользователя' })
|
||||||
|
@ApiResponse({ status: 201 })
|
||||||
|
@Post('create')
|
||||||
|
async createUser(@Body() dto: CreateUserDto): Promise<User> {
|
||||||
|
const newUser = await this.usersService.createUser(
|
||||||
|
dto.firstName,
|
||||||
|
dto.lastName,
|
||||||
|
dto.birthday,
|
||||||
|
dto.sex,
|
||||||
|
dto.problems,
|
||||||
|
);
|
||||||
|
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Заполнить таблицу с пользователями' })
|
||||||
|
@ApiResponse({ status: 201 })
|
||||||
|
@Post('create-all')
|
||||||
|
async createAll(@Body('count') count: number): Promise<{
|
||||||
|
executionTime: number;
|
||||||
|
recordsCreated: number;
|
||||||
|
}> {
|
||||||
|
return await this.usersService.createAll(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Получить список пользователей' })
|
||||||
|
@ApiResponse({ status: 200 })
|
||||||
|
@Get('/')
|
||||||
|
async getUsers(@Query() dto: PaginationDto): Promise<{
|
||||||
|
users: User[];
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
totalUsers: number;
|
||||||
|
}> {
|
||||||
|
const users = await this.usersService.getUsers(dto.page, dto.pageSize);
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Решить все проблемы у пользователей' })
|
||||||
|
@ApiResponse({ status: 201 })
|
||||||
|
@Patch('reset-problems')
|
||||||
|
async resetProblems(): Promise<{
|
||||||
|
solvedProblems: number;
|
||||||
|
executionTime: number;
|
||||||
|
}> {
|
||||||
|
const problems = await this.usersService.resetProblems();
|
||||||
|
return problems;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Очистить таблицу пользователей' })
|
||||||
|
@ApiResponse({ status: 200 })
|
||||||
|
@Delete('delete-all')
|
||||||
|
async deleteAll(): Promise<{ deleteAll: boolean }> {
|
||||||
|
return await this.usersService.deleteAll();
|
||||||
|
}
|
||||||
|
}
|
3
users/src/users/user.interfaces.ts
Executable file
3
users/src/users/user.interfaces.ts
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
export interface Sex {
|
||||||
|
value: 0 | 1 | 2;
|
||||||
|
}
|
48
users/src/users/user.model.ts
Executable file
48
users/src/users/user.model.ts
Executable file
@ -0,0 +1,48 @@
|
|||||||
|
import { Column, DataType, Model, Table } from 'sequelize-typescript';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
interface UserCreationAttrs {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
birthday: string;
|
||||||
|
sex: number;
|
||||||
|
problems: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Table({ tableName: 'user' })
|
||||||
|
export class User extends Model<User, UserCreationAttrs> {
|
||||||
|
@ApiProperty({ example: 1, description: 'Уникальный идентификатор' })
|
||||||
|
@Column({
|
||||||
|
type: DataType.INTEGER,
|
||||||
|
unique: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
})
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Alice', description: 'First Name' })
|
||||||
|
@Column({ type: DataType.STRING, allowNull: false })
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Smith', description: 'Last Name' })
|
||||||
|
@Column({ type: DataType.STRING, allowNull: false })
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: '2006-11-19 19:00:00.000 +00:00',
|
||||||
|
description: 'Birthday',
|
||||||
|
})
|
||||||
|
@Column(DataType.STRING)
|
||||||
|
birthday: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 1, description: 'Sex' })
|
||||||
|
@Column({
|
||||||
|
type: DataType.INTEGER,
|
||||||
|
defaultValue: 0,
|
||||||
|
})
|
||||||
|
sex: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: true, description: 'Problems' })
|
||||||
|
@Column({ type: DataType.BOOLEAN, defaultValue: false })
|
||||||
|
problems: boolean;
|
||||||
|
}
|
12
users/src/users/user.module.ts
Executable file
12
users/src/users/user.module.ts
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { UsersService } from './user.service';
|
||||||
|
import { UsersController } from './user.controller';
|
||||||
|
import { SequelizeModule } from '@nestjs/sequelize';
|
||||||
|
import { User } from './user.model';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [UsersController],
|
||||||
|
providers: [UsersService],
|
||||||
|
imports: [SequelizeModule.forFeature([User])],
|
||||||
|
})
|
||||||
|
export class UsersModule {}
|
128
users/src/users/user.service.ts
Executable file
128
users/src/users/user.service.ts
Executable file
@ -0,0 +1,128 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectModel } from '@nestjs/sequelize';
|
||||||
|
import { User } from './user.model';
|
||||||
|
import { Sequelize } from 'sequelize-typescript';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersService {
|
||||||
|
constructor(
|
||||||
|
@InjectModel(User) private userRepository: typeof User,
|
||||||
|
private sequelize: Sequelize,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createUser(
|
||||||
|
firstName: string,
|
||||||
|
lastName: string,
|
||||||
|
birthday: string,
|
||||||
|
sex: number,
|
||||||
|
problems: boolean,
|
||||||
|
): Promise<User> {
|
||||||
|
const user = await this.userRepository.create({
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
birthday,
|
||||||
|
sex,
|
||||||
|
problems,
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createAll(
|
||||||
|
count: number = 1000000,
|
||||||
|
): Promise<{ executionTime: number; recordsCreated: number }> {
|
||||||
|
const chunkSize = 10000;
|
||||||
|
let totalCreated = 0;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i += chunkSize) {
|
||||||
|
const users = Array.from(
|
||||||
|
{ length: Math.min(chunkSize, count - i) },
|
||||||
|
() => ({
|
||||||
|
firstName:
|
||||||
|
'Firstname_' +
|
||||||
|
(Math.floor(Math.random() * 9_000_000_000) + 1_000_000_000),
|
||||||
|
lastName:
|
||||||
|
'Lastname_' +
|
||||||
|
(Math.floor(Math.random() * 9_000_000_000) + 1_000_000_000),
|
||||||
|
birthday: '2006-11-19 19:00:00.000 +00:00',
|
||||||
|
sex: Math.floor(Math.random() * 2) + 1,
|
||||||
|
problems: Math.random() < 0.5,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const created = await this.userRepository.bulkCreate(users);
|
||||||
|
totalCreated += created.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endTime = Date.now();
|
||||||
|
return {
|
||||||
|
executionTime: endTime - startTime,
|
||||||
|
recordsCreated: totalCreated,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUsers(
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 10,
|
||||||
|
): Promise<{
|
||||||
|
users: User[];
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
totalUsers: number;
|
||||||
|
}> {
|
||||||
|
const offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
|
const users = await this.userRepository.findAndCountAll({
|
||||||
|
limit: pageSize,
|
||||||
|
offset,
|
||||||
|
});
|
||||||
|
const totalPages = Math.ceil(users.count / pageSize);
|
||||||
|
return {
|
||||||
|
users: users.rows,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages,
|
||||||
|
totalUsers: users.count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetProblems(): Promise<{
|
||||||
|
solvedProblems: number;
|
||||||
|
executionTime: number;
|
||||||
|
}> {
|
||||||
|
const transaction = await this.sequelize.transaction();
|
||||||
|
try {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const count = await this.userRepository.count({
|
||||||
|
where: {
|
||||||
|
problems: true,
|
||||||
|
},
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
await this.userRepository.update(
|
||||||
|
{ problems: false },
|
||||||
|
{ where: {}, transaction },
|
||||||
|
);
|
||||||
|
await transaction.commit();
|
||||||
|
const endTime = Date.now();
|
||||||
|
return {
|
||||||
|
solvedProblems: count,
|
||||||
|
executionTime: endTime - startTime,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAll(): Promise<{ deleteAll: boolean }> {
|
||||||
|
try {
|
||||||
|
await this.userRepository.destroy({ truncate: true });
|
||||||
|
return { deleteAll: true };
|
||||||
|
} catch (error) {
|
||||||
|
return { deleteAll: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
users/test/app.e2e-spec.ts
Normal file
28
users/test/app.e2e-spec.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import * as request from 'supertest';
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
import { AppModule } from './../src/app.module';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleFixture = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
9
users/test/jest-e2e.json
Normal file
9
users/test/jest-e2e.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": "\\.e2e-spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
4
users/tsconfig.build.json
Normal file
4
users/tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
||||||
|
}
|
17
users/tsconfig.json
Normal file
17
users/tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2021",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user