migrate ci and postgress
This commit is contained in:
@ -1,6 +0,0 @@
|
|||||||
node_modules
|
|
||||||
npm-debug.log
|
|
||||||
dist
|
|
||||||
.env
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
56
.drone.yml
Normal file
56
.drone.yml
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: kubernetes
|
||||||
|
name: ci
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: prepare
|
||||||
|
image: alpine:3.19
|
||||||
|
environment:
|
||||||
|
GITEA_TOKEN:
|
||||||
|
from_secret: GITEA_TOKEN
|
||||||
|
commands:
|
||||||
|
- apk add --no-cache git bash yq
|
||||||
|
- git clone --depth 1 https://token:$GITEA_TOKEN@git.vigdorov.ru/vigdorov/ci-templates.git .ci
|
||||||
|
- chmod +x .ci/scripts/*.sh
|
||||||
|
- bash .ci/scripts/prepare.sh
|
||||||
|
|
||||||
|
- name: build
|
||||||
|
image: gcr.io/kaniko-project/executor:v1.23.2-debug
|
||||||
|
depends_on: [prepare]
|
||||||
|
environment:
|
||||||
|
HARBOR_USER:
|
||||||
|
from_secret: HARBOR_USER
|
||||||
|
HARBOR_PASSWORD:
|
||||||
|
from_secret: HARBOR_PASSWORD
|
||||||
|
commands:
|
||||||
|
- /busybox/sh .ci/scripts/build.sh
|
||||||
|
|
||||||
|
- name: deploy
|
||||||
|
image: alpine:3.19
|
||||||
|
depends_on: [build]
|
||||||
|
environment:
|
||||||
|
KUBE_CONFIG:
|
||||||
|
from_secret: KUBE_CONFIG
|
||||||
|
commands:
|
||||||
|
- apk add --no-cache bash yq kubectl helm
|
||||||
|
- bash .ci/scripts/deploy.sh
|
||||||
|
|
||||||
|
- name: notify
|
||||||
|
image: appleboy/drone-telegram
|
||||||
|
depends_on: [deploy]
|
||||||
|
settings:
|
||||||
|
token:
|
||||||
|
from_secret: TELEGRAM_TOKEN
|
||||||
|
to:
|
||||||
|
from_secret: TELEGRAM_CHAT_ID
|
||||||
|
format: markdown
|
||||||
|
message: >
|
||||||
|
{{#success build.status}}✅{{else}}❌{{/success}} **{{repo.name}}**
|
||||||
|
Branch: `{{commit.branch}}`
|
||||||
|
{{commit.message}}
|
||||||
|
when:
|
||||||
|
status: [success, failure]
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
branch: [master, develop]
|
||||||
|
event: [push, custom]
|
||||||
24
.eslintrc.js
24
.eslintrc.js
@ -1,24 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
project: 'tsconfig.json',
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
|
||||||
extends: [
|
|
||||||
'plugin:@typescript-eslint/eslint-recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'prettier',
|
|
||||||
'prettier/@typescript-eslint',
|
|
||||||
],
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
jest: true,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/interface-name-prefix': 'off',
|
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
26
.gitignore
vendored
26
.gitignore
vendored
@ -1,11 +1,13 @@
|
|||||||
# compiled output
|
# compiled output
|
||||||
/dist
|
/dist
|
||||||
/node_modules
|
/node_modules
|
||||||
|
/build
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
@ -32,3 +34,27 @@ lerna-debug.log*
|
|||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# temp directory
|
||||||
|
.temp
|
||||||
|
.tmp
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Diagnostic reports
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Tools
|
||||||
|
.serena
|
||||||
|
.claude
|
||||||
|
|||||||
27
CLAUDE.md
27
CLAUDE.md
@ -8,19 +8,27 @@ REST API для работы с изображениями. Использует
|
|||||||
|
|
||||||
## Стек
|
## Стек
|
||||||
|
|
||||||
- **Framework:** NestJS
|
- **Framework:** NestJS 11
|
||||||
- **Language:** TypeScript
|
- **Language:** TypeScript
|
||||||
- **Runtime:** Node.js
|
- **Runtime:** Node.js
|
||||||
|
- **Database:** PostgreSQL (shared-db) + TypeORM
|
||||||
|
- **CI/CD:** Drone CI + ci-templates
|
||||||
|
|
||||||
## Структура
|
## Структура
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── main.ts # Точка входа
|
├── main.ts # Точка входа
|
||||||
├── app.module.ts # Корневой модуль
|
├── app.module.ts # Корневой модуль (ConfigModule + TypeORM)
|
||||||
├── app.controller.ts # REST контроллер
|
├── app.controller.ts # REST контроллер
|
||||||
├── app.service.ts # Бизнес-логика
|
├── app.service.ts # Бизнес-логика (TypeORM Repository)
|
||||||
├── schemas.ts # Валидация данных
|
├── entities/
|
||||||
|
│ ├── author.entity.ts # Entity: authors
|
||||||
|
│ └── image.entity.ts # Entity: images
|
||||||
|
├── health/
|
||||||
|
│ ├── health.module.ts # Health check модуль
|
||||||
|
│ └── health.controller.ts # GET /health
|
||||||
|
├── schemas.ts # Swagger DTO
|
||||||
├── types.ts # TypeScript типы
|
├── types.ts # TypeScript типы
|
||||||
├── consts.ts # Константы
|
├── consts.ts # Константы
|
||||||
└── api.responses.ts # Форматы ответов API
|
└── api.responses.ts # Форматы ответов API
|
||||||
@ -33,12 +41,17 @@ npm install # Установка зависимостей
|
|||||||
npm run start:dev # Запуск в dev режиме
|
npm run start:dev # Запуск в dev режиме
|
||||||
npm run build # Сборка
|
npm run build # Сборка
|
||||||
npm run start:prod # Production запуск
|
npm run start:prod # Production запуск
|
||||||
npm run test # Тесты
|
|
||||||
npm run lint # Линтер
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Деплой
|
## Деплой
|
||||||
|
|
||||||
- **Dockerfile:** есть
|
- **CI/CD:** Drone CI через ci-templates (service.yaml + .drone.yml)
|
||||||
- **Namespace:** backend-for-learning
|
- **Namespace:** backend-for-learning
|
||||||
- **URL:** https://image-list.vigdorov.ru
|
- **URL:** https://image-list.vigdorov.ru
|
||||||
|
- **Health:** GET /health
|
||||||
|
|
||||||
|
## Локальная разработка
|
||||||
|
|
||||||
|
Переменные окружения в `.env.local`:
|
||||||
|
- `DATABASE_HOST` — хост PostgreSQL (localhost для dev через NodePort :30432)
|
||||||
|
- `DATABASE_PORT`, `DATABASE_USER`, `DATABASE_PASSWORD`, `DATABASE_NAME`
|
||||||
|
|||||||
23
Dockerfile
23
Dockerfile
@ -1,23 +0,0 @@
|
|||||||
# Этап сборки
|
|
||||||
FROM node:18-alpine AS build
|
|
||||||
WORKDIR /app
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci
|
|
||||||
COPY . .
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Этап продакшна
|
|
||||||
FROM node:18-alpine
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=build /app/package*.json ./
|
|
||||||
COPY --from=build /app/dist ./dist
|
|
||||||
RUN npm ci --only=production
|
|
||||||
|
|
||||||
# Переменные окружения
|
|
||||||
ENV NODE_ENV production
|
|
||||||
|
|
||||||
# Пользователь без привилегий для безопасности
|
|
||||||
USER node
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
CMD ["node", "dist/main"]
|
|
||||||
35
eslint.config.mjs
Normal file
35
eslint.config.mjs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// @ts-check
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||||
|
import globals from 'globals';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ['eslint.config.mjs'],
|
||||||
|
},
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
ecmaVersion: 5,
|
||||||
|
sourceType: 'module',
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
18509
package-lock.json
generated
18509
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
82
package.json
82
package.json
@ -1,21 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "image-back",
|
"name": "image-back",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prebuild": "rimraf dist",
|
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/main",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"_example_imageBuild": "docker buildx build --platform linux/amd64 -t vigdorov/image-back:0.0.1-amd64 .",
|
|
||||||
"_example_imagePush": "docker push vigdorov/image-back:0.0.1-amd64",
|
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
@ -23,41 +20,45 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^7.0.0",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/core": "^7.0.0",
|
"@nestjs/config": "^4.0.0",
|
||||||
"@nestjs/mongoose": "^7.1.2",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^7.0.0",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/swagger": "^4.7.5",
|
"@nestjs/swagger": "^11.0.6",
|
||||||
"mongoose": "^5.11.4",
|
"@nestjs/terminus": "^11.0.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
"rimraf": "^3.0.2",
|
"pg": "^8.13.1",
|
||||||
"rxjs": "^6.5.4",
|
"reflect-metadata": "^0.2.2",
|
||||||
"swagger-ui-express": "^4.1.5",
|
"rxjs": "^7.8.1",
|
||||||
"uuid": "^8.3.1"
|
"typeorm": "^0.3.20",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^7.0.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@nestjs/schematics": "^7.0.0",
|
"@eslint/js": "^9.18.0",
|
||||||
"@nestjs/testing": "^7.0.0",
|
"@nestjs/cli": "^11.0.0",
|
||||||
"@types/express": "^4.17.3",
|
"@nestjs/schematics": "^11.0.0",
|
||||||
"@types/jest": "25.2.3",
|
"@nestjs/testing": "^11.0.1",
|
||||||
"@types/mongoose": "^5.10.2",
|
"@swc/cli": "^0.6.0",
|
||||||
"@types/node": "^13.9.1",
|
"@swc/core": "^1.10.7",
|
||||||
"@types/supertest": "^2.0.8",
|
"@types/express": "^5.0.0",
|
||||||
"@types/uuid": "^8.3.0",
|
"@types/jest": "^29.5.14",
|
||||||
"@typescript-eslint/eslint-plugin": "3.0.2",
|
"@types/node": "^22.10.7",
|
||||||
"@typescript-eslint/parser": "3.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"eslint": "7.1.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^6.10.0",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-import": "^2.20.1",
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
"jest": "26.0.1",
|
"globals": "^15.14.0",
|
||||||
"prettier": "^1.19.1",
|
"jest": "^29.7.0",
|
||||||
"supertest": "^4.0.2",
|
"prettier": "^3.4.2",
|
||||||
"ts-jest": "26.1.0",
|
"source-map-support": "^0.5.21",
|
||||||
"ts-loader": "^6.2.1",
|
"supertest": "^7.0.0",
|
||||||
"ts-node": "^8.6.2",
|
"ts-jest": "^29.2.5",
|
||||||
"tsconfig-paths": "^3.9.0",
|
"ts-loader": "^9.5.2",
|
||||||
"typescript": "^3.7.4"
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.0"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
@ -66,10 +67,13 @@
|
|||||||
"ts"
|
"ts"
|
||||||
],
|
],
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"testRegex": ".spec.ts$",
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
},
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
"coverageDirectory": "../coverage",
|
"coverageDirectory": "../coverage",
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node"
|
||||||
}
|
}
|
||||||
|
|||||||
24
service.yaml
Normal file
24
service.yaml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
service:
|
||||||
|
name: image-list
|
||||||
|
type: api
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
namespace: backend-for-learning
|
||||||
|
domain: image-list.vigdorov.ru
|
||||||
|
|
||||||
|
infrastructure:
|
||||||
|
postgres: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
- name: DATABASE_HOST
|
||||||
|
value: "image-list-postgres"
|
||||||
|
- name: DATABASE_PORT
|
||||||
|
value: "5432"
|
||||||
|
- name: DATABASE_USER
|
||||||
|
value: "image_list_user"
|
||||||
|
- name: DATABASE_PASSWORD
|
||||||
|
value: "Im@g3L1st_DB_P@ss_2025!"
|
||||||
|
- name: DATABASE_NAME
|
||||||
|
value: "image_list_db"
|
||||||
|
- name: CORS_ORIGIN
|
||||||
|
value: "*"
|
||||||
@ -1,45 +1,46 @@
|
|||||||
import {ApiProperty, ApiResponseOptions} from '@nestjs/swagger';
|
import { ApiProperty, ApiResponseOptions } from '@nestjs/swagger';
|
||||||
import {ImageResponse} from './schemas';
|
import { ImageResponse } from './schemas';
|
||||||
|
|
||||||
class Error {
|
class ErrorResponse {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
message: string;
|
message: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AUTH_ERROR: ApiResponseOptions = {
|
export const AUTH_ERROR: ApiResponseOptions = {
|
||||||
status: 406,
|
status: 406,
|
||||||
description: 'Ошибка, при попытке получить доступ к данным без токена или с не корректным токеном',
|
description:
|
||||||
type: Error,
|
'Ошибка, при попытке получить доступ к данным без токена или с не корректным токеном',
|
||||||
|
type: ErrorResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GET_IMAGE_LIST_SUCCESS: ApiResponseOptions = {
|
export const GET_IMAGE_LIST_SUCCESS: ApiResponseOptions = {
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'Список всех картинок',
|
description: 'Список всех картинок',
|
||||||
type: ImageResponse,
|
type: ImageResponse,
|
||||||
isArray: true
|
isArray: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GET_USER_LIST_SUCCESS: ApiResponseOptions = {
|
export const GET_USER_LIST_SUCCESS: ApiResponseOptions = {
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'Список всех пользователей',
|
description: 'Список всех пользователей',
|
||||||
type: String,
|
type: String,
|
||||||
isArray: true
|
isArray: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MANIPULATE_IMAGE_SUCCESS: ApiResponseOptions = {
|
export const MANIPULATE_IMAGE_SUCCESS: ApiResponseOptions = {
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'Картинка',
|
description: 'Картинка',
|
||||||
type: ImageResponse,
|
type: ImageResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AUTH_SUCCESS: ApiResponseOptions = {
|
export const AUTH_SUCCESS: ApiResponseOptions = {
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'Токен пользователя',
|
description: 'Токен пользователя',
|
||||||
type: String,
|
type: String,
|
||||||
};
|
};
|
||||||
@ -1,133 +1,166 @@
|
|||||||
import {Controller, Delete, Get, Header, HttpCode, Options, Post, Put, Req} from '@nestjs/common';
|
import {
|
||||||
import {AppService} from './app.service';
|
Controller,
|
||||||
import {AuthRequest, ImageCreateRequest} from './schemas';
|
Delete,
|
||||||
import {Request} from 'express';
|
Get,
|
||||||
import {ApiBody, ApiParam, ApiQuery, ApiResponse, ApiSecurity, ApiTags} from '@nestjs/swagger';
|
Header,
|
||||||
import {Image, ImageCreate} from './types';
|
HttpCode,
|
||||||
import {ALLOW_CREDENTIALS, ALLOW_HEADERS, ALLOW_METHOD, ALLOW_ORIGIN_ALL, CONTENT_LENGTH} from './consts';
|
Options,
|
||||||
import {AUTH_ERROR, AUTH_SUCCESS, GET_IMAGE_LIST_SUCCESS, GET_USER_LIST_SUCCESS, MANIPULATE_IMAGE_SUCCESS} from './api.responses';
|
Post,
|
||||||
|
Put,
|
||||||
|
Req,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
import {
|
||||||
|
ApiBody,
|
||||||
|
ApiExcludeEndpoint,
|
||||||
|
ApiParam,
|
||||||
|
ApiQuery,
|
||||||
|
ApiResponse,
|
||||||
|
ApiSecurity,
|
||||||
|
ApiTags,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { Image, ImageCreate } from './types';
|
||||||
|
import {
|
||||||
|
ALLOW_CREDENTIALS,
|
||||||
|
ALLOW_HEADERS,
|
||||||
|
ALLOW_METHOD,
|
||||||
|
ALLOW_ORIGIN_ALL,
|
||||||
|
CONTENT_LENGTH,
|
||||||
|
} from './consts';
|
||||||
|
import {
|
||||||
|
AUTH_ERROR,
|
||||||
|
AUTH_SUCCESS,
|
||||||
|
GET_IMAGE_LIST_SUCCESS,
|
||||||
|
GET_USER_LIST_SUCCESS,
|
||||||
|
MANIPULATE_IMAGE_SUCCESS,
|
||||||
|
} from './api.responses';
|
||||||
|
import { AuthRequest, ImageCreateRequest } from './schemas';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
@ApiTags('image-app')
|
@ApiTags('image-app')
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(private readonly appService: AppService) { }
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
@Post('/auth')
|
@Post('/auth')
|
||||||
@ApiBody({
|
@ApiBody({
|
||||||
type: AuthRequest,
|
type: AuthRequest,
|
||||||
description: 'Объект с логином пользователя',
|
description: 'Объект с логином пользователя',
|
||||||
})
|
})
|
||||||
@Header(...ALLOW_ORIGIN_ALL)
|
@Header(...ALLOW_ORIGIN_ALL)
|
||||||
@ApiResponse(AUTH_SUCCESS)
|
@ApiResponse(AUTH_SUCCESS)
|
||||||
authUser(
|
authUser(
|
||||||
@Req() request: Request<null, {login: string}>
|
@Req() request: Request<null, null, { login: string }>,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return this.appService.authUser(request.body.login);
|
return this.appService.authUser(request.body.login);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/list')
|
@Get('/list')
|
||||||
@ApiSecurity('apiKey')
|
@ApiSecurity('apiKey')
|
||||||
@Header(...ALLOW_ORIGIN_ALL)
|
@Header(...ALLOW_ORIGIN_ALL)
|
||||||
@ApiResponse(GET_IMAGE_LIST_SUCCESS)
|
@ApiResponse(GET_IMAGE_LIST_SUCCESS)
|
||||||
@ApiResponse(AUTH_ERROR)
|
@ApiResponse(AUTH_ERROR)
|
||||||
async getImageList(
|
async getImageList(@Req() request: Request): Promise<Image[]> {
|
||||||
@Req() request: Request
|
await this.appService.checkRequest(request.headers.authorization);
|
||||||
): Promise<Image[]> {
|
return this.appService.getImageList();
|
||||||
await this.appService.checkRequest(request.headers.authorization);
|
}
|
||||||
return this.appService.getImageList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('/users')
|
@Get('/users')
|
||||||
@ApiSecurity('apiKey')
|
@ApiSecurity('apiKey')
|
||||||
@Header(...ALLOW_ORIGIN_ALL)
|
@Header(...ALLOW_ORIGIN_ALL)
|
||||||
@ApiQuery({
|
@ApiQuery({
|
||||||
name: 'login',
|
name: 'login',
|
||||||
description: 'Часть логина пользователя',
|
description: 'Часть логина пользователя',
|
||||||
required: false,
|
required: false,
|
||||||
})
|
})
|
||||||
@ApiResponse(GET_USER_LIST_SUCCESS)
|
@ApiResponse(GET_USER_LIST_SUCCESS)
|
||||||
@ApiResponse(AUTH_ERROR)
|
@ApiResponse(AUTH_ERROR)
|
||||||
async getUserList(
|
async getUserList(
|
||||||
@Req() request: Request<null, null, null, {login?: string}>
|
@Req() request: Request<null, null, null, { login?: string }>,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
await this.appService.checkRequest(request.headers.authorization);
|
await this.appService.checkRequest(request.headers.authorization);
|
||||||
return this.appService.getUserList(request.query.login ?? '');
|
return this.appService.getUserList(request.query.login ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/list/:id')
|
@Get('/list/:id')
|
||||||
@ApiSecurity('apiKey')
|
@ApiSecurity('apiKey')
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
description: 'id картинки',
|
description: 'id картинки',
|
||||||
})
|
})
|
||||||
@Header(...ALLOW_ORIGIN_ALL)
|
@Header(...ALLOW_ORIGIN_ALL)
|
||||||
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
|
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
|
||||||
@ApiResponse(AUTH_ERROR)
|
@ApiResponse(AUTH_ERROR)
|
||||||
async getImageById(
|
async getImageById(
|
||||||
@Req() request: Request<{id: string}>
|
@Req() request: Request<{ id: string }>,
|
||||||
): Promise<Image> {
|
): Promise<Image> {
|
||||||
await this.appService.checkRequest(request.headers.authorization);
|
await this.appService.checkRequest(request.headers.authorization);
|
||||||
return this.appService.getImageById(request.params.id);
|
return this.appService.getImageById(request.params.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/list')
|
@Post('/list')
|
||||||
@ApiSecurity('apiKey')
|
@ApiSecurity('apiKey')
|
||||||
@ApiBody({
|
@ApiBody({
|
||||||
type: ImageCreateRequest,
|
type: ImageCreateRequest,
|
||||||
description: 'Объект создания картинки',
|
description: 'Объект создания картинки',
|
||||||
})
|
})
|
||||||
@Header(...ALLOW_ORIGIN_ALL)
|
@Header(...ALLOW_ORIGIN_ALL)
|
||||||
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
|
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
|
||||||
@ApiResponse(AUTH_ERROR)
|
@ApiResponse(AUTH_ERROR)
|
||||||
async createImage(
|
async createImage(
|
||||||
@Req() request: Request<null, ImageCreate>
|
@Req() request: Request<null, null, ImageCreate>,
|
||||||
): Promise<Image> {
|
): Promise<Image> {
|
||||||
const {login} = await this.appService.checkRequest(request.headers.authorization);
|
const { login } = await this.appService.checkRequest(
|
||||||
return this.appService.addImage(login, request.body)
|
request.headers.authorization,
|
||||||
}
|
);
|
||||||
|
return this.appService.addImage(login, request.body);
|
||||||
|
}
|
||||||
|
|
||||||
@Put('/list/:id')
|
@Put('/list/:id')
|
||||||
@ApiSecurity('apiKey')
|
@ApiSecurity('apiKey')
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
description: 'id картинки',
|
description: 'id картинки',
|
||||||
})
|
})
|
||||||
@Header(...ALLOW_ORIGIN_ALL)
|
@Header(...ALLOW_ORIGIN_ALL)
|
||||||
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
|
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
|
||||||
@ApiResponse(AUTH_ERROR)
|
@ApiResponse(AUTH_ERROR)
|
||||||
async toggleLike(
|
async toggleLike(
|
||||||
@Req() request: Request<{id: string}>
|
@Req() request: Request<{ id: string }>,
|
||||||
): Promise<Image> {
|
): Promise<Image> {
|
||||||
const {login} = await this.appService.checkRequest(request.headers.authorization);
|
const { login } = await this.appService.checkRequest(
|
||||||
return this.appService.toggleLike(login, request.params.id);
|
request.headers.authorization,
|
||||||
}
|
);
|
||||||
|
return this.appService.toggleLike(login, request.params.id);
|
||||||
|
}
|
||||||
|
|
||||||
@Delete('/list/:id')
|
@Delete('/list/:id')
|
||||||
@ApiSecurity('apiKey')
|
@ApiSecurity('apiKey')
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
description: 'id картинки',
|
description: 'id картинки',
|
||||||
})
|
})
|
||||||
@Header(...ALLOW_ORIGIN_ALL)
|
@Header(...ALLOW_ORIGIN_ALL)
|
||||||
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
|
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
|
||||||
@ApiResponse(AUTH_ERROR)
|
@ApiResponse(AUTH_ERROR)
|
||||||
async deleteImage(
|
async deleteImage(
|
||||||
@Req() request: Request<{id: string}>
|
@Req() request: Request<{ id: string }>,
|
||||||
): Promise<Image> {
|
): Promise<Image> {
|
||||||
const {login} = await this.appService.checkRequest(request.headers.authorization);
|
const { login } = await this.appService.checkRequest(
|
||||||
return this.appService.deleteImageById(login, request.params.id);
|
request.headers.authorization,
|
||||||
}
|
);
|
||||||
|
return this.appService.deleteImageById(login, request.params.id);
|
||||||
|
}
|
||||||
|
|
||||||
@Options([
|
@ApiExcludeEndpoint()
|
||||||
'', '/auth', '/list', '/list/:id'
|
@Options(['', '/auth', '/list', '/list/:id'])
|
||||||
])
|
@Header(...ALLOW_ORIGIN_ALL)
|
||||||
@Header(...ALLOW_ORIGIN_ALL)
|
@Header(...ALLOW_METHOD)
|
||||||
@Header(...ALLOW_METHOD)
|
@Header(...ALLOW_CREDENTIALS)
|
||||||
@Header(...ALLOW_CREDENTIALS)
|
@Header(...CONTENT_LENGTH)
|
||||||
@Header(...CONTENT_LENGTH)
|
@Header(...ALLOW_HEADERS)
|
||||||
@Header(...ALLOW_HEADERS)
|
@HttpCode(204)
|
||||||
@HttpCode(204)
|
async options(): Promise<string> {
|
||||||
async options(): Promise<string> {
|
return await Promise.resolve('');
|
||||||
return '';
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +1,36 @@
|
|||||||
import {Module} from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import {MongooseModule} from '@nestjs/mongoose/dist/mongoose.module';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import {AppController} from './app.controller';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import {AppService} from './app.service';
|
import { AppController } from './app.controller';
|
||||||
import {DB_IMAGES, DB_AUTHORS, MONGO_URL} from './consts';
|
import { AppService } from './app.service';
|
||||||
import {AuthorDocument, AuthorScheme, ImageDocument, ImageScheme} from './schemas';
|
import { AuthorEntity } from './entities/author.entity';
|
||||||
|
import { ImageEntity } from './entities/image.entity';
|
||||||
|
import { HealthModule } from './health/health.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
MongooseModule.forRoot(`${MONGO_URL}/${DB_AUTHORS}`, {
|
ConfigModule.forRoot({
|
||||||
connectionName: DB_AUTHORS,
|
isGlobal: true,
|
||||||
}),
|
envFilePath: ['.env.local', '.env'],
|
||||||
MongooseModule.forRoot(`${MONGO_URL}/${DB_IMAGES}`, {
|
}),
|
||||||
connectionName: DB_IMAGES,
|
TypeOrmModule.forRootAsync({
|
||||||
}),
|
imports: [ConfigModule],
|
||||||
MongooseModule.forFeature([
|
inject: [ConfigService],
|
||||||
{name: AuthorDocument.name, schema: AuthorScheme},
|
useFactory: (configService: ConfigService) => ({
|
||||||
], DB_AUTHORS),
|
type: 'postgres',
|
||||||
MongooseModule.forFeature([
|
host: configService.get('DATABASE_HOST', 'localhost'),
|
||||||
{name: ImageDocument.name, schema: ImageScheme},
|
port: configService.get('DATABASE_PORT', 5432),
|
||||||
], DB_IMAGES),
|
username: configService.get('DATABASE_USER', 'postgres'),
|
||||||
],
|
password: configService.get('DATABASE_PASSWORD', 'postgres'),
|
||||||
controllers: [AppController],
|
database: configService.get('DATABASE_NAME', 'image_list_db'),
|
||||||
providers: [AppService],
|
entities: [AuthorEntity, ImageEntity],
|
||||||
|
synchronize: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
TypeOrmModule.forFeature([AuthorEntity, ImageEntity]),
|
||||||
|
HealthModule,
|
||||||
|
],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule {}
|
||||||
|
|||||||
@ -1,141 +1,142 @@
|
|||||||
import {BadRequestException, Injectable, NotAcceptableException} from '@nestjs/common';
|
import {
|
||||||
import {InjectModel} from '@nestjs/mongoose';
|
BadRequestException,
|
||||||
import {Model} from 'mongoose';
|
Injectable,
|
||||||
import {v4} from 'uuid';
|
NotAcceptableException,
|
||||||
import {AuthorDocument, ImageDocument} from './schemas';
|
} from '@nestjs/common';
|
||||||
import {Author, Image, ImageCreate} from './types';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { ILike, Repository } from 'typeorm';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import { AuthorEntity } from './entities/author.entity';
|
||||||
|
import { ImageEntity } from './entities/image.entity';
|
||||||
|
import { Author, Image, ImageCreate } from './types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppService {
|
export class AppService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectModel(AuthorDocument.name) private authorModel: Model<AuthorDocument>,
|
@InjectRepository(AuthorEntity)
|
||||||
@InjectModel(ImageDocument.name) private imageModel: Model<ImageDocument>,
|
private authorRepository: Repository<AuthorEntity>,
|
||||||
) { }
|
@InjectRepository(ImageEntity)
|
||||||
|
private imageRepository: Repository<ImageEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
async checkRequest(token?: string): Promise<Author> {
|
async checkRequest(token?: string): Promise<Author> {
|
||||||
const authorList = await this.authorModel.find().exec();
|
const searchAuthor = await this.authorRepository.findOne({
|
||||||
const searchAuthor = authorList.find((author) => author.token === token);
|
where: { token },
|
||||||
if (searchAuthor) {
|
});
|
||||||
return {
|
if (searchAuthor) {
|
||||||
login: searchAuthor.login,
|
return {
|
||||||
token: searchAuthor.token,
|
login: searchAuthor.login,
|
||||||
};
|
token: searchAuthor.token,
|
||||||
}
|
};
|
||||||
throw new NotAcceptableException(`Доступ запрещен`);
|
}
|
||||||
|
throw new NotAcceptableException(`Доступ запрещен`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getImageList(): Promise<Image[]> {
|
||||||
|
const imageList = await this.imageRepository.find({
|
||||||
|
order: { createdAt: 'DESC' },
|
||||||
|
});
|
||||||
|
return imageList.map(({ url, author, likes, id, createdAt }) => ({
|
||||||
|
url,
|
||||||
|
author,
|
||||||
|
likes,
|
||||||
|
id,
|
||||||
|
create_at: createdAt.toISOString(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserList(searchLogin: string): Promise<string[]> {
|
||||||
|
const authorList = await this.authorRepository.find({
|
||||||
|
where: searchLogin
|
||||||
|
? { login: ILike(`%${searchLogin}%`) }
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
return authorList.map(({ login }) => login);
|
||||||
|
}
|
||||||
|
|
||||||
|
async authUser(login: string): Promise<string> {
|
||||||
|
const searchAuthor = await this.authorRepository.findOne({
|
||||||
|
where: { login },
|
||||||
|
});
|
||||||
|
if (searchAuthor) {
|
||||||
|
return searchAuthor.token;
|
||||||
|
}
|
||||||
|
const newAuthor = await this.authorRepository.save({
|
||||||
|
login,
|
||||||
|
token: v4(),
|
||||||
|
});
|
||||||
|
return newAuthor.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addImage(login: string, image: ImageCreate): Promise<Image> {
|
||||||
|
if (!image.url) {
|
||||||
|
throw new BadRequestException('url обязателен');
|
||||||
|
}
|
||||||
|
const newImage = await this.imageRepository.save({
|
||||||
|
url: image.url,
|
||||||
|
likes: [],
|
||||||
|
author: login,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
url: newImage.url,
|
||||||
|
author: newImage.author,
|
||||||
|
likes: newImage.likes,
|
||||||
|
id: newImage.id,
|
||||||
|
create_at: newImage.createdAt.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getImageById(id: string): Promise<Image> {
|
||||||
|
const searchImage = await this.imageRepository.findOne({ where: { id } });
|
||||||
|
if (searchImage) {
|
||||||
|
return {
|
||||||
|
url: searchImage.url,
|
||||||
|
author: searchImage.author,
|
||||||
|
likes: searchImage.likes,
|
||||||
|
id: searchImage.id,
|
||||||
|
create_at: searchImage.createdAt.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new BadRequestException(`Картинка с id - "${id}" не найдена`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteImageById(login: string, id: string): Promise<Image> {
|
||||||
|
const searchImage = await this.imageRepository.findOne({ where: { id } });
|
||||||
|
if (!searchImage) {
|
||||||
|
throw new BadRequestException(`Картинка с id - "${id}" не найдена`);
|
||||||
|
}
|
||||||
|
if (searchImage.author !== login) {
|
||||||
|
throw new NotAcceptableException(`Нельзя удалить чужую картинку`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getImageList(): Promise<Image[]> {
|
await this.imageRepository.delete(id);
|
||||||
const imageList = await this.imageModel.find().exec();
|
|
||||||
return imageList.map(({url, author, likes, id, create_at}) => ({
|
return {
|
||||||
url,
|
url: searchImage.url,
|
||||||
author,
|
author: searchImage.author,
|
||||||
likes,
|
likes: searchImage.likes,
|
||||||
id,
|
id: searchImage.id,
|
||||||
create_at,
|
create_at: searchImage.createdAt.toISOString(),
|
||||||
}));
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleLike(login: string, id: string): Promise<Image> {
|
||||||
|
const searchImage = await this.imageRepository.findOne({ where: { id } });
|
||||||
|
if (!searchImage) {
|
||||||
|
throw new BadRequestException(`Картинка с id - "${id}" не найдена`);
|
||||||
}
|
}
|
||||||
|
const updatedLikes = searchImage.likes.includes(login)
|
||||||
|
? searchImage.likes.filter((userLogin) => userLogin !== login)
|
||||||
|
: searchImage.likes.concat(login);
|
||||||
|
|
||||||
async getUserList(searchLogin: string): Promise<string[]> {
|
await this.imageRepository.update(id, { likes: updatedLikes });
|
||||||
const authorList = await this.authorModel.find().exec();
|
|
||||||
return authorList.reduce((acc, {login}) => {
|
|
||||||
if (login.includes(searchLogin)) {
|
|
||||||
acc.push(login);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
async authUser(login: string): Promise<string> {
|
return {
|
||||||
const authorList = await this.authorModel.find().exec();
|
url: searchImage.url,
|
||||||
const searchAuthor = authorList.find((author) => author.login === login);
|
author: searchImage.author,
|
||||||
if (searchAuthor) {
|
likes: updatedLikes,
|
||||||
return searchAuthor.token;
|
id: searchImage.id,
|
||||||
}
|
create_at: searchImage.createdAt.toISOString(),
|
||||||
const Model = await this.authorModel;
|
};
|
||||||
const userModel = new Model({
|
}
|
||||||
login,
|
|
||||||
token: v4(),
|
|
||||||
});
|
|
||||||
const newUser = await userModel.save();
|
|
||||||
return newUser.token;
|
|
||||||
}
|
|
||||||
|
|
||||||
async addImage(login: string, image: ImageCreate): Promise<Image> {
|
|
||||||
const Model = await this.imageModel;
|
|
||||||
const imageModel = new Model({
|
|
||||||
url: image.url,
|
|
||||||
likes: [],
|
|
||||||
author: login,
|
|
||||||
create_at: new Date().toISOString()
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await imageModel.validate();
|
|
||||||
} catch (e) {
|
|
||||||
throw new BadRequestException(e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newImage = await imageModel.save();
|
|
||||||
return {
|
|
||||||
url: newImage.url,
|
|
||||||
author: newImage.author,
|
|
||||||
likes: [],
|
|
||||||
id: newImage.id,
|
|
||||||
create_at: newImage.create_at,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getImageById(id: string): Promise<Image> {
|
|
||||||
const searchImage = await this.imageModel.findById(id);
|
|
||||||
if (searchImage) {
|
|
||||||
return {
|
|
||||||
url: searchImage.url,
|
|
||||||
author: searchImage.author,
|
|
||||||
likes: searchImage.likes,
|
|
||||||
id: searchImage.id,
|
|
||||||
create_at: searchImage.create_at,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
throw new BadRequestException(`Картинка с id - "${id}" не найдена`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteImageById(login: string, id: string): Promise<Image> {
|
|
||||||
const searchImage = await this.imageModel.findById(id);
|
|
||||||
if (!searchImage) {
|
|
||||||
throw new BadRequestException(`Картинка с id - "${id}" не найдена`);
|
|
||||||
}
|
|
||||||
if (searchImage.author !== login) {
|
|
||||||
throw new NotAcceptableException(`Нельзя удалить чужую картинку`);
|
|
||||||
}
|
|
||||||
const Model = await this.imageModel;
|
|
||||||
|
|
||||||
await Model.findByIdAndDelete(id);
|
|
||||||
return {
|
|
||||||
url: searchImage.url,
|
|
||||||
author: searchImage.author,
|
|
||||||
likes: searchImage.likes,
|
|
||||||
id: searchImage.id,
|
|
||||||
create_at: searchImage.create_at,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async toggleLike(login: string, id: string): Promise<Image> {
|
|
||||||
const searchImage = await this.imageModel.findById(id);
|
|
||||||
if (!searchImage) {
|
|
||||||
throw new BadRequestException(`Картинка с id - "${id}" не найдена`);
|
|
||||||
}
|
|
||||||
const updatedLikes = searchImage.likes.includes(login)
|
|
||||||
? searchImage.likes.filter(userLogin => userLogin !== login)
|
|
||||||
: searchImage.likes.concat(login);
|
|
||||||
await searchImage.updateOne({
|
|
||||||
likes: updatedLikes,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: searchImage.url,
|
|
||||||
author: searchImage.author,
|
|
||||||
likes: updatedLikes,
|
|
||||||
id: searchImage.id,
|
|
||||||
create_at: searchImage.create_at,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,3 @@
|
|||||||
// Подключение к MongoDB
|
|
||||||
export const MONGO_URL = process.env.MONGODB_URI || 'mongodb://localhost:27017';
|
|
||||||
|
|
||||||
// Имя базы данных для пользователей и хранилищ
|
|
||||||
export const DB_AUTHORS = process.env.DB_AUTHORS || 'image-back-authors';
|
|
||||||
export const DB_IMAGES = process.env.DB_IMAGES || 'image-back-images';
|
|
||||||
|
|
||||||
// Порт приложения
|
// Порт приложения
|
||||||
export const APP_PORT = parseInt(process.env.APP_PORT || '3000', 10);
|
export const APP_PORT = parseInt(process.env.APP_PORT || '3000', 10);
|
||||||
|
|
||||||
@ -13,11 +6,19 @@ export const APP_CONTROLLER = 'image-app';
|
|||||||
|
|
||||||
// CORS настройки
|
// CORS настройки
|
||||||
export const ALLOW_ORIGIN_ALL: [string, string] = [
|
export const ALLOW_ORIGIN_ALL: [string, string] = [
|
||||||
'Access-Control-Allow-Origin',
|
'Access-Control-Allow-Origin',
|
||||||
process.env.CORS_ORIGIN || '*',
|
process.env.CORS_ORIGIN || '*',
|
||||||
];
|
];
|
||||||
|
export const ALLOW_CREDENTIALS: [string, string] = [
|
||||||
export const ALLOW_CREDENTIALS: [string, string] = ['Access-Control-Allow-Credentials', 'true'];
|
'Access-Control-Allow-Credentials',
|
||||||
|
'true',
|
||||||
|
];
|
||||||
export const CONTENT_LENGTH: [string, string] = ['Content-Length', '0'];
|
export const CONTENT_LENGTH: [string, string] = ['Content-Length', '0'];
|
||||||
export const ALLOW_METHOD: [string, string] = ['Access-Control-Allow-Methods', 'GET,HEAD,PUT,PATCH,POST,DELETE'];
|
export const ALLOW_METHOD: [string, string] = [
|
||||||
export const ALLOW_HEADERS: [string, string] = ['Access-Control-Allow-Headers', 'Version, Authorization, Content-Type, Api-Name, x-turbo-id, x-turbo-compression, chrome-proxy, chrome-proxy-ect'];
|
'Access-Control-Allow-Methods',
|
||||||
|
'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||||
|
];
|
||||||
|
export const ALLOW_HEADERS: [string, string] = [
|
||||||
|
'Access-Control-Allow-Headers',
|
||||||
|
'Version, Authorization, Content-Type, Api-Name, x-turbo-id, x-turbo-compression, chrome-proxy, chrome-proxy-ect',
|
||||||
|
];
|
||||||
|
|||||||
13
src/entities/author.entity.ts
Normal file
13
src/entities/author.entity.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('authors')
|
||||||
|
export class AuthorEntity {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', unique: true })
|
||||||
|
login: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar' })
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
19
src/entities/image.entity.ts
Normal file
19
src/entities/image.entity.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('images')
|
||||||
|
export class ImageEntity {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text' })
|
||||||
|
url: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar' })
|
||||||
|
author: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', array: true, default: '{}' })
|
||||||
|
likes: string[];
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'timestamp' })
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
20
src/health/health.controller.ts
Normal file
20
src/health/health.controller.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
HealthCheckService,
|
||||||
|
HealthCheck,
|
||||||
|
TypeOrmHealthIndicator,
|
||||||
|
} from '@nestjs/terminus';
|
||||||
|
|
||||||
|
@Controller('health')
|
||||||
|
export class HealthController {
|
||||||
|
constructor(
|
||||||
|
private health: HealthCheckService,
|
||||||
|
private db: TypeOrmHealthIndicator,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@HealthCheck()
|
||||||
|
check() {
|
||||||
|
return this.health.check([() => this.db.pingCheck('database')]);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/health/health.module.ts
Normal file
9
src/health/health.module.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TerminusModule } from '@nestjs/terminus';
|
||||||
|
import { HealthController } from './health.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TerminusModule],
|
||||||
|
controllers: [HealthController],
|
||||||
|
})
|
||||||
|
export class HealthModule {}
|
||||||
@ -25,4 +25,5 @@ async function bootstrap() {
|
|||||||
|
|
||||||
await app.listen(APP_PORT);
|
await app.listen(APP_PORT);
|
||||||
}
|
}
|
||||||
bootstrap();
|
|
||||||
|
void bootstrap();
|
||||||
|
|||||||
@ -1,84 +1,37 @@
|
|||||||
import {ApiProperty} from '@nestjs/swagger/dist/decorators';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import {Document} from 'mongoose';
|
import { Author, Image } from './types';
|
||||||
import {Prop, Schema, SchemaFactory} from '@nestjs/mongoose';
|
|
||||||
import {Author, Image} from './types';
|
|
||||||
|
|
||||||
export class AuthRequest {
|
export class AuthRequest {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
login: string;
|
login: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ImageCreateRequest {
|
export class ImageCreateRequest {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthorResponse implements Author {
|
export class AuthorResponse implements Author {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
login: string;
|
login: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ImageResponse implements Image {
|
export class ImageResponse implements Image {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
url: string;
|
url: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
author: string;
|
author: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
likes: string[];
|
likes: string[];
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
create_at: string;
|
create_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Schema()
|
|
||||||
export class AuthorDocument extends Document {
|
|
||||||
@Prop({
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
})
|
|
||||||
login: string;
|
|
||||||
|
|
||||||
@Prop({
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
})
|
|
||||||
token: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Schema()
|
|
||||||
export class ImageDocument extends Document {
|
|
||||||
@Prop({
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
})
|
|
||||||
url: string;
|
|
||||||
|
|
||||||
@Prop({
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
})
|
|
||||||
author: string;
|
|
||||||
|
|
||||||
@Prop({
|
|
||||||
type: [String],
|
|
||||||
required: true,
|
|
||||||
})
|
|
||||||
likes: string[];
|
|
||||||
|
|
||||||
@Prop({
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
})
|
|
||||||
create_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ImageScheme = SchemaFactory.createForClass(ImageDocument);
|
|
||||||
export const AuthorScheme = SchemaFactory.createForClass(AuthorDocument);
|
|
||||||
|
|||||||
@ -6,11 +6,16 @@
|
|||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"target": "es2017",
|
"target": "ES2023",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user