Compare commits
5 Commits
4236e665a2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c098f39dd5 | |||
| 9eaf58aca5 | |||
| a9ce5cc03b | |||
| 9d2d30e991 | |||
| 448b63a59f |
@ -1,6 +0,0 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
dist
|
||||
.env
|
||||
.git
|
||||
.gitignore
|
||||
41
.drone.yml
Normal file
41
.drone.yml
Normal file
@ -0,0 +1,41 @@
|
||||
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
|
||||
|
||||
|
||||
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',
|
||||
},
|
||||
};
|
||||
28
.gitignore
vendored
28
.gitignore
vendored
@ -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*
|
||||
@ -31,4 +33,28 @@ lerna-debug.log*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.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
|
||||
|
||||
1
.npmrc
Normal file
1
.npmrc
Normal file
@ -0,0 +1 @@
|
||||
@vigdorov:registry=https://git.vigdorov.ru/api/packages/vigdorov/npm/
|
||||
@ -1,4 +1 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
"@vigdorov/prettier-config"
|
||||
|
||||
57
CLAUDE.md
Normal file
57
CLAUDE.md
Normal file
@ -0,0 +1,57 @@
|
||||
# Image List Backend
|
||||
|
||||
Учебный backend-проект — упрощенный Instagram для обучения студентов.
|
||||
|
||||
## Назначение
|
||||
|
||||
REST API для работы с изображениями. Используется как практический проект при обучении backend-разработке на 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 # Корневой модуль (ConfigModule + TypeORM)
|
||||
├── app.controller.ts # REST контроллер
|
||||
├── 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
|
||||
```
|
||||
|
||||
## Команды
|
||||
|
||||
```bash
|
||||
npm install # Установка зависимостей
|
||||
npm run start:dev # Запуск в dev режиме
|
||||
npm run build # Сборка
|
||||
npm run start:prod # Production запуск
|
||||
```
|
||||
|
||||
## Деплой
|
||||
|
||||
- **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`
|
||||
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"]
|
||||
2
eslint.config.mjs
Normal file
2
eslint.config.mjs
Normal file
@ -0,0 +1,2 @@
|
||||
import {node} from '@vigdorov/eslint-config';
|
||||
export default node();
|
||||
20066
package-lock.json
generated
20066
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
85
package.json
85
package.json
@ -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,48 @@
|
||||
"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",
|
||||
"@vigdorov/eslint-config": "^1.0.1",
|
||||
"@vigdorov/prettier-config": "^1.0.0",
|
||||
"@vigdorov/typescript-config": "^1.1.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 +70,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
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: "ImageList_DB_2025"
|
||||
- name: DATABASE_NAME
|
||||
value: "image_list_db"
|
||||
- name: CORS_ORIGIN
|
||||
value: "*"
|
||||
@ -1,45 +1,46 @@
|
||||
import {ApiProperty, ApiResponseOptions} from '@nestjs/swagger';
|
||||
import {ImageResponse} from './schemas';
|
||||
import { ApiProperty, ApiResponseOptions } from '@nestjs/swagger';
|
||||
import { ImageResponse } from './schemas';
|
||||
|
||||
class Error {
|
||||
@ApiProperty()
|
||||
statusCode: number;
|
||||
class ErrorResponse {
|
||||
@ApiProperty()
|
||||
statusCode: number;
|
||||
|
||||
@ApiProperty()
|
||||
message: string;
|
||||
@ApiProperty()
|
||||
message: string;
|
||||
|
||||
@ApiProperty()
|
||||
error: string;
|
||||
@ApiProperty()
|
||||
error: string;
|
||||
}
|
||||
|
||||
export const AUTH_ERROR: ApiResponseOptions = {
|
||||
status: 406,
|
||||
description: 'Ошибка, при попытке получить доступ к данным без токена или с не корректным токеном',
|
||||
type: Error,
|
||||
status: 406,
|
||||
description:
|
||||
'Ошибка, при попытке получить доступ к данным без токена или с не корректным токеном',
|
||||
type: ErrorResponse,
|
||||
};
|
||||
|
||||
export const GET_IMAGE_LIST_SUCCESS: ApiResponseOptions = {
|
||||
status: 200,
|
||||
description: 'Список всех картинок',
|
||||
type: ImageResponse,
|
||||
isArray: true
|
||||
status: 200,
|
||||
description: 'Список всех картинок',
|
||||
type: ImageResponse,
|
||||
isArray: true,
|
||||
};
|
||||
|
||||
export const GET_USER_LIST_SUCCESS: ApiResponseOptions = {
|
||||
status: 200,
|
||||
description: 'Список всех пользователей',
|
||||
type: String,
|
||||
isArray: true
|
||||
status: 200,
|
||||
description: 'Список всех пользователей',
|
||||
type: String,
|
||||
isArray: true,
|
||||
};
|
||||
|
||||
export const MANIPULATE_IMAGE_SUCCESS: ApiResponseOptions = {
|
||||
status: 200,
|
||||
description: 'Картинка',
|
||||
type: ImageResponse,
|
||||
status: 200,
|
||||
description: 'Картинка',
|
||||
type: ImageResponse,
|
||||
};
|
||||
|
||||
export const AUTH_SUCCESS: ApiResponseOptions = {
|
||||
status: 200,
|
||||
description: 'Токен пользователя',
|
||||
type: String,
|
||||
};
|
||||
status: 200,
|
||||
description: 'Токен пользователя',
|
||||
type: String,
|
||||
};
|
||||
|
||||
@ -1,133 +1,166 @@
|
||||
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({
|
||||
type: AuthRequest,
|
||||
description: 'Объект с логином пользователя',
|
||||
})
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiResponse(AUTH_SUCCESS)
|
||||
authUser(
|
||||
@Req() request: Request<null, {login: string}>
|
||||
): Promise<string> {
|
||||
return this.appService.authUser(request.body.login);
|
||||
}
|
||||
@Post('/auth')
|
||||
@ApiBody({
|
||||
type: AuthRequest,
|
||||
description: 'Объект с логином пользователя',
|
||||
})
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiResponse(AUTH_SUCCESS)
|
||||
authUser(
|
||||
@Req() request: Request<null, null, { login: string }>,
|
||||
): Promise<string> {
|
||||
return this.appService.authUser(request.body.login);
|
||||
}
|
||||
|
||||
@Get('/list')
|
||||
@ApiSecurity('apiKey')
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiResponse(GET_IMAGE_LIST_SUCCESS)
|
||||
@ApiResponse(AUTH_ERROR)
|
||||
async getImageList(
|
||||
@Req() request: Request
|
||||
): Promise<Image[]> {
|
||||
await this.appService.checkRequest(request.headers.authorization);
|
||||
return this.appService.getImageList();
|
||||
}
|
||||
@Get('/list')
|
||||
@ApiSecurity('apiKey')
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiResponse(GET_IMAGE_LIST_SUCCESS)
|
||||
@ApiResponse(AUTH_ERROR)
|
||||
async getImageList(@Req() request: Request): Promise<Image[]> {
|
||||
await this.appService.checkRequest(request.headers.authorization);
|
||||
return this.appService.getImageList();
|
||||
}
|
||||
|
||||
@Get('/users')
|
||||
@ApiSecurity('apiKey')
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiQuery({
|
||||
name: 'login',
|
||||
description: 'Часть логина пользователя',
|
||||
required: false,
|
||||
})
|
||||
@ApiResponse(GET_USER_LIST_SUCCESS)
|
||||
@ApiResponse(AUTH_ERROR)
|
||||
async getUserList(
|
||||
@Req() request: Request<null, null, null, {login?: string}>
|
||||
): Promise<string[]> {
|
||||
await this.appService.checkRequest(request.headers.authorization);
|
||||
return this.appService.getUserList(request.query.login ?? '');
|
||||
}
|
||||
@Get('/users')
|
||||
@ApiSecurity('apiKey')
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiQuery({
|
||||
name: 'login',
|
||||
description: 'Часть логина пользователя',
|
||||
required: false,
|
||||
})
|
||||
@ApiResponse(GET_USER_LIST_SUCCESS)
|
||||
@ApiResponse(AUTH_ERROR)
|
||||
async getUserList(
|
||||
@Req() request: Request<null, null, null, { login?: string }>,
|
||||
): Promise<string[]> {
|
||||
await this.appService.checkRequest(request.headers.authorization);
|
||||
return this.appService.getUserList(request.query.login ?? '');
|
||||
}
|
||||
|
||||
@Get('/list/:id')
|
||||
@ApiSecurity('apiKey')
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: 'id картинки',
|
||||
})
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
|
||||
@ApiResponse(AUTH_ERROR)
|
||||
async getImageById(
|
||||
@Req() request: Request<{id: string}>
|
||||
): Promise<Image> {
|
||||
await this.appService.checkRequest(request.headers.authorization);
|
||||
return this.appService.getImageById(request.params.id);
|
||||
}
|
||||
@Get('/list/:id')
|
||||
@ApiSecurity('apiKey')
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: 'id картинки',
|
||||
})
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
|
||||
@ApiResponse(AUTH_ERROR)
|
||||
async getImageById(
|
||||
@Req() request: Request<{ id: string }>,
|
||||
): Promise<Image> {
|
||||
await this.appService.checkRequest(request.headers.authorization);
|
||||
return this.appService.getImageById(request.params.id);
|
||||
}
|
||||
|
||||
@Post('/list')
|
||||
@ApiSecurity('apiKey')
|
||||
@ApiBody({
|
||||
type: ImageCreateRequest,
|
||||
description: 'Объект создания картинки',
|
||||
})
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
|
||||
@ApiResponse(AUTH_ERROR)
|
||||
async createImage(
|
||||
@Req() request: Request<null, ImageCreate>
|
||||
): Promise<Image> {
|
||||
const {login} = await this.appService.checkRequest(request.headers.authorization);
|
||||
return this.appService.addImage(login, request.body)
|
||||
}
|
||||
@Post('/list')
|
||||
@ApiSecurity('apiKey')
|
||||
@ApiBody({
|
||||
type: ImageCreateRequest,
|
||||
description: 'Объект создания картинки',
|
||||
})
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
|
||||
@ApiResponse(AUTH_ERROR)
|
||||
async createImage(
|
||||
@Req() request: Request<null, null, ImageCreate>,
|
||||
): Promise<Image> {
|
||||
const { login } = await this.appService.checkRequest(
|
||||
request.headers.authorization,
|
||||
);
|
||||
return this.appService.addImage(login, request.body);
|
||||
}
|
||||
|
||||
@Put('/list/:id')
|
||||
@ApiSecurity('apiKey')
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: 'id картинки',
|
||||
})
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
|
||||
@ApiResponse(AUTH_ERROR)
|
||||
async toggleLike(
|
||||
@Req() request: Request<{id: string}>
|
||||
): Promise<Image> {
|
||||
const {login} = await this.appService.checkRequest(request.headers.authorization);
|
||||
return this.appService.toggleLike(login, request.params.id);
|
||||
}
|
||||
@Put('/list/:id')
|
||||
@ApiSecurity('apiKey')
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: 'id картинки',
|
||||
})
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
|
||||
@ApiResponse(AUTH_ERROR)
|
||||
async toggleLike(
|
||||
@Req() request: Request<{ id: string }>,
|
||||
): Promise<Image> {
|
||||
const { login } = await this.appService.checkRequest(
|
||||
request.headers.authorization,
|
||||
);
|
||||
return this.appService.toggleLike(login, request.params.id);
|
||||
}
|
||||
|
||||
@Delete('/list/:id')
|
||||
@ApiSecurity('apiKey')
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: 'id картинки',
|
||||
})
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
|
||||
@ApiResponse(AUTH_ERROR)
|
||||
async deleteImage(
|
||||
@Req() request: Request<{id: string}>
|
||||
): Promise<Image> {
|
||||
const {login} = await this.appService.checkRequest(request.headers.authorization);
|
||||
return this.appService.deleteImageById(login, request.params.id);
|
||||
}
|
||||
|
||||
@Options([
|
||||
'', '/auth', '/list', '/list/:id'
|
||||
])
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@Header(...ALLOW_METHOD)
|
||||
@Header(...ALLOW_CREDENTIALS)
|
||||
@Header(...CONTENT_LENGTH)
|
||||
@Header(...ALLOW_HEADERS)
|
||||
@HttpCode(204)
|
||||
async options(): Promise<string> {
|
||||
return '';
|
||||
}
|
||||
@Delete('/list/:id')
|
||||
@ApiSecurity('apiKey')
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: 'id картинки',
|
||||
})
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiResponse(MANIPULATE_IMAGE_SUCCESS)
|
||||
@ApiResponse(AUTH_ERROR)
|
||||
async deleteImage(
|
||||
@Req() request: Request<{ id: string }>,
|
||||
): Promise<Image> {
|
||||
const { login } = await this.appService.checkRequest(
|
||||
request.headers.authorization,
|
||||
);
|
||||
return this.appService.deleteImageById(login, request.params.id);
|
||||
}
|
||||
|
||||
@ApiExcludeEndpoint()
|
||||
@Options(['', '/auth', '/list', '/list/:id'])
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@Header(...ALLOW_METHOD)
|
||||
@Header(...ALLOW_CREDENTIALS)
|
||||
@Header(...CONTENT_LENGTH)
|
||||
@Header(...ALLOW_HEADERS)
|
||||
@HttpCode(204)
|
||||
async options(): Promise<string> {
|
||||
return await Promise.resolve('');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
MongooseModule.forRoot(`${MONGO_URL}/${DB_IMAGES}`, {
|
||||
connectionName: DB_IMAGES,
|
||||
}),
|
||||
MongooseModule.forFeature([
|
||||
{name: AuthorDocument.name, schema: AuthorScheme},
|
||||
], DB_AUTHORS),
|
||||
MongooseModule.forFeature([
|
||||
{name: ImageDocument.name, schema: ImageScheme},
|
||||
], DB_IMAGES),
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: ['.env.local', '.env'],
|
||||
}),
|
||||
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,
|
||||
}),
|
||||
}),
|
||||
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 {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>,
|
||||
) { }
|
||||
constructor(
|
||||
@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);
|
||||
if (searchAuthor) {
|
||||
return {
|
||||
login: searchAuthor.login,
|
||||
token: searchAuthor.token,
|
||||
};
|
||||
}
|
||||
throw new NotAcceptableException(`Доступ запрещен`);
|
||||
async checkRequest(token?: string): Promise<Author> {
|
||||
const searchAuthor = await this.authorRepository.findOne({
|
||||
where: { token },
|
||||
});
|
||||
if (searchAuthor) {
|
||||
return {
|
||||
login: searchAuthor.login,
|
||||
token: searchAuthor.token,
|
||||
};
|
||||
}
|
||||
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[]> {
|
||||
const imageList = await this.imageModel.find().exec();
|
||||
return imageList.map(({url, author, likes, id, create_at}) => ({
|
||||
url,
|
||||
author,
|
||||
likes,
|
||||
id,
|
||||
create_at,
|
||||
}));
|
||||
await this.imageRepository.delete(id);
|
||||
|
||||
return {
|
||||
url: searchImage.url,
|
||||
author: searchImage.author,
|
||||
likes: searchImage.likes,
|
||||
id: searchImage.id,
|
||||
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[]> {
|
||||
const authorList = await this.authorModel.find().exec();
|
||||
return authorList.reduce((acc, {login}) => {
|
||||
if (login.includes(searchLogin)) {
|
||||
acc.push(login);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
await this.imageRepository.update(id, { likes: updatedLikes });
|
||||
|
||||
async authUser(login: string): Promise<string> {
|
||||
const authorList = await this.authorModel.find().exec();
|
||||
const searchAuthor = authorList.find((author) => author.login === login);
|
||||
if (searchAuthor) {
|
||||
return searchAuthor.token;
|
||||
}
|
||||
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,
|
||||
};
|
||||
}
|
||||
return {
|
||||
url: searchImage.url,
|
||||
author: searchImage.author,
|
||||
likes: updatedLikes,
|
||||
id: searchImage.id,
|
||||
create_at: searchImage.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -13,11 +6,19 @@ export const APP_CONTROLLER = 'image-app';
|
||||
|
||||
// CORS настройки
|
||||
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'];
|
||||
'Access-Control-Allow-Origin',
|
||||
process.env.CORS_ORIGIN || '*',
|
||||
];
|
||||
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',
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
void bootstrap();
|
||||
|
||||
@ -1,84 +1,37 @@
|
||||
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()
|
||||
login: string;
|
||||
@ApiProperty()
|
||||
login: string;
|
||||
}
|
||||
|
||||
export class ImageCreateRequest {
|
||||
@ApiProperty()
|
||||
url: string;
|
||||
@ApiProperty()
|
||||
url: string;
|
||||
}
|
||||
|
||||
export class AuthorResponse implements Author {
|
||||
@ApiProperty()
|
||||
login: string;
|
||||
@ApiProperty()
|
||||
login: string;
|
||||
|
||||
@ApiProperty()
|
||||
token: string;
|
||||
@ApiProperty()
|
||||
token: string;
|
||||
}
|
||||
|
||||
export class ImageResponse implements Image {
|
||||
@ApiProperty()
|
||||
url: string;
|
||||
@ApiProperty()
|
||||
url: string;
|
||||
|
||||
@ApiProperty()
|
||||
author: string;
|
||||
@ApiProperty()
|
||||
author: string;
|
||||
|
||||
@ApiProperty()
|
||||
likes: string[];
|
||||
@ApiProperty()
|
||||
likes: string[];
|
||||
|
||||
@ApiProperty()
|
||||
id: string;
|
||||
@ApiProperty()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
create_at: string;
|
||||
@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);
|
||||
|
||||
@ -1,16 +1,12 @@
|
||||
{
|
||||
"extends": "@vigdorov/typescript-config/node",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es2017",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true
|
||||
"strict": false,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user