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