migrate ci and postgress
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is failing

This commit is contained in:
2026-02-08 20:09:53 +03:00
parent 448b63a59f
commit 9d2d30e991
22 changed files with 11409 additions and 8243 deletions

View File

@ -1,6 +0,0 @@
node_modules
npm-debug.log
dist
.env
.git
.gitignore

56
.drone.yml Normal file
View 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]

View File

@ -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
View File

@ -1,11 +1,13 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
@ -32,3 +34,27 @@ lerna-debug.log*
!.vscode/tasks.json
!.vscode/launch.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

View File

@ -8,19 +8,27 @@ REST API для работы с изображениями. Использует
## Стек
- **Framework:** NestJS
- **Framework:** NestJS 11
- **Language:** TypeScript
- **Runtime:** Node.js
- **Database:** PostgreSQL (shared-db) + TypeORM
- **CI/CD:** Drone CI + ci-templates
## Структура
```
src/
├── main.ts # Точка входа
├── app.module.ts # Корневой модуль
├── app.module.ts # Корневой модуль (ConfigModule + TypeORM)
├── app.controller.ts # REST контроллер
├── app.service.ts # Бизнес-логика
├── schemas.ts # Валидация данных
├── app.service.ts # Бизнес-логика (TypeORM Repository)
├── 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 типы
├── consts.ts # Константы
└── api.responses.ts # Форматы ответов API
@ -33,12 +41,17 @@ npm install # Установка зависимостей
npm run start:dev # Запуск в dev режиме
npm run build # Сборка
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
- **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`

View File

@ -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
View 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',
},
},
);

18749
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,18 @@
{
"name": "image-back",
"version": "0.1.0",
"version": "0.2.0",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"dev": "nest start --watch",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"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:watch": "jest --watch",
"test:cov": "jest --coverage",
@ -23,41 +20,45 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^7.0.0",
"@nestjs/core": "^7.0.0",
"@nestjs/mongoose": "^7.1.2",
"@nestjs/platform-express": "^7.0.0",
"@nestjs/swagger": "^4.7.5",
"mongoose": "^5.11.4",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^6.5.4",
"swagger-ui-express": "^4.1.5",
"uuid": "^8.3.1"
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.0",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.0.6",
"@nestjs/terminus": "^11.0.0",
"@nestjs/typeorm": "^11.0.0",
"pg": "^8.13.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.20",
"uuid": "^11.1.0"
},
"devDependencies": {
"@nestjs/cli": "^7.0.0",
"@nestjs/schematics": "^7.0.0",
"@nestjs/testing": "^7.0.0",
"@types/express": "^4.17.3",
"@types/jest": "25.2.3",
"@types/mongoose": "^5.10.2",
"@types/node": "^13.9.1",
"@types/supertest": "^2.0.8",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "3.0.2",
"@typescript-eslint/parser": "3.0.2",
"eslint": "7.1.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"jest": "26.0.1",
"prettier": "^1.19.1",
"supertest": "^4.0.2",
"ts-jest": "26.1.0",
"ts-loader": "^6.2.1",
"ts-node": "^8.6.2",
"tsconfig-paths": "^3.9.0",
"typescript": "^3.7.4"
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^15.14.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
@ -66,10 +67,13 @@
"ts"
],
"rootDir": "src",
"testRegex": ".spec.ts$",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}

24
service.yaml Normal file
View 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: "*"

View File

@ -1,7 +1,7 @@
import {ApiProperty, ApiResponseOptions} from '@nestjs/swagger';
import {ImageResponse} from './schemas';
import { ApiProperty, ApiResponseOptions } from '@nestjs/swagger';
import { ImageResponse } from './schemas';
class Error {
class ErrorResponse {
@ApiProperty()
statusCode: number;
@ -14,22 +14,23 @@ class Error {
export const AUTH_ERROR: ApiResponseOptions = {
status: 406,
description: 'Ошибка, при попытке получить доступ к данным без токена или с не корректным токеном',
type: Error,
description:
'Ошибка, при попытке получить доступ к данным без токена или с не корректным токеном',
type: ErrorResponse,
};
export const GET_IMAGE_LIST_SUCCESS: ApiResponseOptions = {
status: 200,
description: 'Список всех картинок',
type: ImageResponse,
isArray: true
isArray: true,
};
export const GET_USER_LIST_SUCCESS: ApiResponseOptions = {
status: 200,
description: 'Список всех пользователей',
type: String,
isArray: true
isArray: true,
};
export const MANIPULATE_IMAGE_SUCCESS: ApiResponseOptions = {

View File

@ -1,16 +1,46 @@
import {Controller, Delete, Get, Header, HttpCode, Options, Post, Put, Req} from '@nestjs/common';
import {AppService} from './app.service';
import {AuthRequest, ImageCreateRequest} from './schemas';
import {Request} from 'express';
import {ApiBody, 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 {
Controller,
Delete,
Get,
Header,
HttpCode,
Options,
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()
@ApiTags('image-app')
export class AppController {
constructor(private readonly appService: AppService) { }
constructor(private readonly appService: AppService) {}
@Post('/auth')
@ApiBody({
@ -20,7 +50,7 @@ export class AppController {
@Header(...ALLOW_ORIGIN_ALL)
@ApiResponse(AUTH_SUCCESS)
authUser(
@Req() request: Request<null, {login: string}>
@Req() request: Request<null, null, { login: string }>,
): Promise<string> {
return this.appService.authUser(request.body.login);
}
@ -30,9 +60,7 @@ export class AppController {
@Header(...ALLOW_ORIGIN_ALL)
@ApiResponse(GET_IMAGE_LIST_SUCCESS)
@ApiResponse(AUTH_ERROR)
async getImageList(
@Req() request: Request
): Promise<Image[]> {
async getImageList(@Req() request: Request): Promise<Image[]> {
await this.appService.checkRequest(request.headers.authorization);
return this.appService.getImageList();
}
@ -48,7 +76,7 @@ export class AppController {
@ApiResponse(GET_USER_LIST_SUCCESS)
@ApiResponse(AUTH_ERROR)
async getUserList(
@Req() request: Request<null, null, null, {login?: string}>
@Req() request: Request<null, null, null, { login?: string }>,
): Promise<string[]> {
await this.appService.checkRequest(request.headers.authorization);
return this.appService.getUserList(request.query.login ?? '');
@ -64,7 +92,7 @@ export class AppController {
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
@ApiResponse(AUTH_ERROR)
async getImageById(
@Req() request: Request<{id: string}>
@Req() request: Request<{ id: string }>,
): Promise<Image> {
await this.appService.checkRequest(request.headers.authorization);
return this.appService.getImageById(request.params.id);
@ -80,10 +108,12 @@ export class AppController {
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
@ApiResponse(AUTH_ERROR)
async createImage(
@Req() request: Request<null, ImageCreate>
@Req() request: Request<null, null, ImageCreate>,
): Promise<Image> {
const {login} = await this.appService.checkRequest(request.headers.authorization);
return this.appService.addImage(login, request.body)
const { login } = await this.appService.checkRequest(
request.headers.authorization,
);
return this.appService.addImage(login, request.body);
}
@Put('/list/:id')
@ -96,9 +126,11 @@ export class AppController {
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
@ApiResponse(AUTH_ERROR)
async toggleLike(
@Req() request: Request<{id: string}>
@Req() request: Request<{ id: string }>,
): Promise<Image> {
const {login} = await this.appService.checkRequest(request.headers.authorization);
const { login } = await this.appService.checkRequest(
request.headers.authorization,
);
return this.appService.toggleLike(login, request.params.id);
}
@ -112,15 +144,16 @@ export class AppController {
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
@ApiResponse(AUTH_ERROR)
async deleteImage(
@Req() request: Request<{id: string}>
@Req() request: Request<{ id: string }>,
): Promise<Image> {
const {login} = await this.appService.checkRequest(request.headers.authorization);
const { login } = await this.appService.checkRequest(
request.headers.authorization,
);
return this.appService.deleteImageById(login, request.params.id);
}
@Options([
'', '/auth', '/list', '/list/:id'
])
@ApiExcludeEndpoint()
@Options(['', '/auth', '/list', '/list/:id'])
@Header(...ALLOW_ORIGIN_ALL)
@Header(...ALLOW_METHOD)
@Header(...ALLOW_CREDENTIALS)
@ -128,6 +161,6 @@ export class AppController {
@Header(...ALLOW_HEADERS)
@HttpCode(204)
async options(): Promise<string> {
return '';
return await Promise.resolve('');
}
}

View File

@ -1,26 +1,36 @@
import {Module} from '@nestjs/common';
import {MongooseModule} from '@nestjs/mongoose/dist/mongoose.module';
import {AppController} from './app.controller';
import {AppService} from './app.service';
import {DB_IMAGES, DB_AUTHORS, MONGO_URL} from './consts';
import {AuthorDocument, AuthorScheme, ImageDocument, ImageScheme} from './schemas';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthorEntity } from './entities/author.entity';
import { ImageEntity } from './entities/image.entity';
import { HealthModule } from './health/health.module';
@Module({
imports: [
MongooseModule.forRoot(`${MONGO_URL}/${DB_AUTHORS}`, {
connectionName: DB_AUTHORS,
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.local', '.env'],
}),
MongooseModule.forRoot(`${MONGO_URL}/${DB_IMAGES}`, {
connectionName: DB_IMAGES,
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get('DATABASE_HOST', 'localhost'),
port: configService.get('DATABASE_PORT', 5432),
username: configService.get('DATABASE_USER', 'postgres'),
password: configService.get('DATABASE_PASSWORD', 'postgres'),
database: configService.get('DATABASE_NAME', 'image_list_db'),
entities: [AuthorEntity, ImageEntity],
synchronize: true,
}),
MongooseModule.forFeature([
{name: AuthorDocument.name, schema: AuthorScheme},
], DB_AUTHORS),
MongooseModule.forFeature([
{name: ImageDocument.name, schema: ImageScheme},
], DB_IMAGES),
}),
TypeOrmModule.forFeature([AuthorEntity, ImageEntity]),
HealthModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }
export class AppModule {}

View File

@ -1,20 +1,28 @@
import {BadRequestException, Injectable, NotAcceptableException} from '@nestjs/common';
import {InjectModel} from '@nestjs/mongoose';
import {Model} from 'mongoose';
import {v4} from 'uuid';
import {AuthorDocument, ImageDocument} from './schemas';
import {Author, Image, ImageCreate} from './types';
import {
BadRequestException,
Injectable,
NotAcceptableException,
} from '@nestjs/common';
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()
export class AppService {
constructor(
@InjectModel(AuthorDocument.name) private authorModel: Model<AuthorDocument>,
@InjectModel(ImageDocument.name) private imageModel: Model<ImageDocument>,
) { }
@InjectRepository(AuthorEntity)
private authorRepository: Repository<AuthorEntity>,
@InjectRepository(ImageEntity)
private imageRepository: Repository<ImageEntity>,
) {}
async checkRequest(token?: string): Promise<Author> {
const authorList = await this.authorModel.find().exec();
const searchAuthor = authorList.find((author) => author.token === token);
const searchAuthor = await this.authorRepository.findOne({
where: { token },
});
if (searchAuthor) {
return {
login: searchAuthor.login,
@ -25,117 +33,110 @@ export class AppService {
}
async getImageList(): Promise<Image[]> {
const imageList = await this.imageModel.find().exec();
return imageList.map(({url, author, likes, id, create_at}) => ({
const imageList = await this.imageRepository.find({
order: { createdAt: 'DESC' },
});
return imageList.map(({ url, author, likes, id, createdAt }) => ({
url,
author,
likes,
id,
create_at,
create_at: createdAt.toISOString(),
}));
}
async getUserList(searchLogin: string): Promise<string[]> {
const authorList = await this.authorModel.find().exec();
return authorList.reduce((acc, {login}) => {
if (login.includes(searchLogin)) {
acc.push(login);
}
return acc;
}, []);
const authorList = await this.authorRepository.find({
where: searchLogin
? { login: ILike(`%${searchLogin}%`) }
: undefined,
});
return authorList.map(({ login }) => login);
}
async authUser(login: string): Promise<string> {
const authorList = await this.authorModel.find().exec();
const searchAuthor = authorList.find((author) => author.login === login);
const searchAuthor = await this.authorRepository.findOne({
where: { login },
});
if (searchAuthor) {
return searchAuthor.token;
}
const Model = await this.authorModel;
const userModel = new Model({
const newAuthor = await this.authorRepository.save({
login,
token: v4(),
});
const newUser = await userModel.save();
return newUser.token;
return newAuthor.token;
}
async addImage(login: string, image: ImageCreate): Promise<Image> {
const Model = await this.imageModel;
const imageModel = new Model({
if (!image.url) {
throw new BadRequestException('url обязателен');
}
const newImage = await this.imageRepository.save({
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: [],
likes: newImage.likes,
id: newImage.id,
create_at: newImage.create_at,
create_at: newImage.createdAt.toISOString(),
};
}
async getImageById(id: string): Promise<Image> {
const searchImage = await this.imageModel.findById(id);
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.create_at,
create_at: searchImage.createdAt.toISOString(),
};
}
throw new BadRequestException(`Картинка с id - "${id}" не найдена`);
}
async deleteImageById(login: string, id: string): Promise<Image> {
const searchImage = await this.imageModel.findById(id);
const searchImage = await this.imageRepository.findOne({ where: { id } });
if (!searchImage) {
throw new BadRequestException(`Картинка с id - "${id}" не найдена`);
}
if (searchImage.author !== login) {
throw new NotAcceptableException(`Нельзя удалить чужую картинку`);
}
const Model = await this.imageModel;
await Model.findByIdAndDelete(id);
await this.imageRepository.delete(id);
return {
url: searchImage.url,
author: searchImage.author,
likes: searchImage.likes,
id: searchImage.id,
create_at: searchImage.create_at,
create_at: searchImage.createdAt.toISOString(),
};
}
async toggleLike(login: string, id: string): Promise<Image> {
const searchImage = await this.imageModel.findById(id);
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.filter((userLogin) => userLogin !== login)
: searchImage.likes.concat(login);
await searchImage.updateOne({
likes: updatedLikes,
});
await this.imageRepository.update(id, { likes: updatedLikes });
return {
url: searchImage.url,
author: searchImage.author,
likes: updatedLikes,
id: searchImage.id,
create_at: searchImage.create_at,
create_at: searchImage.createdAt.toISOString(),
};
}
}

View File

@ -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);
@ -15,9 +8,17 @@ export const APP_CONTROLLER = 'image-app';
export const ALLOW_ORIGIN_ALL: [string, string] = [
'Access-Control-Allow-Origin',
process.env.CORS_ORIGIN || '*',
];
export const ALLOW_CREDENTIALS: [string, string] = ['Access-Control-Allow-Credentials', 'true'];
];
export const ALLOW_CREDENTIALS: [string, string] = [
'Access-Control-Allow-Credentials',
'true',
];
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_HEADERS: [string, string] = ['Access-Control-Allow-Headers', 'Version, Authorization, Content-Type, Api-Name, x-turbo-id, x-turbo-compression, chrome-proxy, chrome-proxy-ect'];
export const ALLOW_METHOD: [string, string] = [
'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',
];

View 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;
}

View 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;
}

View 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')]);
}
}

View 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 {}

View File

@ -25,4 +25,5 @@ async function bootstrap() {
await app.listen(APP_PORT);
}
bootstrap();
void bootstrap();

View File

@ -1,7 +1,5 @@
import {ApiProperty} from '@nestjs/swagger/dist/decorators';
import {Document} from 'mongoose';
import {Prop, Schema, SchemaFactory} from '@nestjs/mongoose';
import {Author, Image} from './types';
import { ApiProperty } from '@nestjs/swagger';
import { Author, Image } from './types';
export class AuthRequest {
@ApiProperty()
@ -37,48 +35,3 @@ export class ImageResponse implements Image {
@ApiProperty()
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);

View File

@ -6,11 +6,16 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true
"skipLibCheck": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false
}
}