Commit fcdb6611 authored by Kulpybaev Ilyas's avatar Kulpybaev Ilyas

lesson 87

parent 6628fe08
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
...@@ -506,6 +507,11 @@ ...@@ -506,6 +507,11 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/validator": {
"version": "13.11.9",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz",
"integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw=="
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
...@@ -1064,6 +1070,16 @@ ...@@ -1064,6 +1070,16 @@
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw=="
}, },
"node_modules/class-validator": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz",
"integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==",
"dependencies": {
"@types/validator": "^13.11.8",
"libphonenumber-js": "^1.10.53",
"validator": "^13.9.0"
}
},
"node_modules/cli-highlight": { "node_modules/cli-highlight": {
"version": "2.1.11", "version": "2.1.11",
"resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz",
...@@ -2354,6 +2370,11 @@ ...@@ -2354,6 +2370,11 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/libphonenumber-js": {
"version": "1.10.60",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.60.tgz",
"integrity": "sha512-Ctgq2lXUpEJo5j1762NOzl2xo7z7pqmVWYai0p07LvAkQ32tbPv3wb+tcUeHEiXhKU5buM4H9MXsXo6OlM6C2g=="
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
...@@ -3845,6 +3866,14 @@ ...@@ -3845,6 +3866,14 @@
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="
}, },
"node_modules/validator": {
"version": "13.11.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
"integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
......
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { Product } from '@/entities/product.entity'; import { Product } from '@/entities/product.entity';
import { Category } from '@/entities/category.entity';
export const appDataSource = new DataSource({ export const appDataSource = new DataSource({
type: 'mysql', type: 'mysql',
...@@ -10,5 +11,5 @@ export const appDataSource = new DataSource({ ...@@ -10,5 +11,5 @@ export const appDataSource = new DataSource({
database: 'test', database: 'test',
synchronize: true, synchronize: true,
logging: true, logging: true,
entities: [Product], entities: [Product, Category],
}); });
import { CategoryService } from '@/services/category.service';
import { RequestHandler } from 'express';
import { plainToInstance } from 'class-transformer';
import { CategoryDto } from '@/dto/category.dto';
export class CategoryController {
private service: CategoryService;
constructor() {
this.service = new CategoryService();
}
getCategories: RequestHandler = async (req, res): Promise<void> => {
const categories = await this.service.getCategories();
res.send(categories);
};
createCategory: RequestHandler = async (req, res): Promise<void> => {
const categoryDto = plainToInstance(CategoryDto, req.body);
const category = await this.service.createCategory(categoryDto);
res.send(category);
};
}
import { RequestHandler } from 'express'; import { RequestHandler } from 'express';
import { ProductService } from '@/services/product.service'; import { ProductService } from '@/services/product.service';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { ProductDto } from '@/dto/ProductDto'; import { ProductDto } from '@/dto/product.dto';
export class ProductController { export class ProductController {
private service: ProductService; private service: ProductService;
...@@ -25,9 +25,18 @@ export class ProductController { ...@@ -25,9 +25,18 @@ export class ProductController {
}; };
createProduct: RequestHandler = async (req, res): Promise<void> => { createProduct: RequestHandler = async (req, res): Promise<void> => {
const productDto = plainToInstance(ProductDto, req.body); try {
if (req.file) productDto.image = req.file.filename; const productDto = plainToInstance(ProductDto, req.body);
const product = await this.service.createProduct(productDto); if (req.file) productDto.image = req.file.filename;
res.send(product); const product = await this.service.createProduct(productDto);
res.send(product);
} catch (e) {
if (Array.isArray(e)) {
console.log(e);
res.status(400).send(e);
} else {
res.status(500).send(e);
}
}
}; };
} }
import { Expose } from 'class-transformer'; import { Expose } from 'class-transformer';
@Expose()
export class ProductDto { export class CategoryDto {
@Expose()
title!: string; title!: string;
@Expose()
description!: string; description!: string;
price!: number;
image!: string;
} }
import { Expose } from 'class-transformer';
import { IsNotEmpty, IsNumberString, IsOptional, IsString } from 'class-validator';
@Expose()
export class ProductDto {
@IsNotEmpty({ message: 'Продукт не может быть создан без названия!' })
@IsString({ message: 'Название должно быть строкой' })
title!: string;
@IsOptional()
description!: string;
@IsNotEmpty({ message: 'Укажите цену продукта' })
@IsNumberString({}, { message: 'Укажите корректную цену' })
price!: number;
@IsOptional()
image!: string;
}
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity('categories')
export class Category {
@PrimaryGeneratedColumn()
id!: number;
@Column()
title!: string;
@Column({ nullable: true })
description?: string;
}
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from 'typeorm';
import { Category } from '@/entities/category.entity';
@Entity({ name: 'products' }) @Entity({ name: 'products' })
export class Product { export class Product {
...@@ -16,4 +17,11 @@ export class Product { ...@@ -16,4 +17,11 @@ export class Product {
@Column() @Column()
description!: string; description!: string;
@ManyToOne(() => Category)
@JoinColumn({ name: 'categoryId' })
category!: Category;
@Column({ nullable: true })
categoryId?: number;
} }
import { ValidationError } from 'class-validator';
interface IError {
type: string;
messages: string[];
}
export const formatErrors = (errors: ValidationError[]) => {
const updatedErrors: IError[] = [];
errors.forEach((e) => {
if (e.constraints) {
const error: IError = {
type: e.property,
messages: Object.values(e.constraints),
};
updatedErrors.push(error);
}
});
return updatedErrors;
};
...@@ -3,11 +3,12 @@ import logger from './middlewares/logger'; ...@@ -3,11 +3,12 @@ import logger from './middlewares/logger';
import { ArticleRoute } from './routes/article.route'; import { ArticleRoute } from './routes/article.route';
import { ProductRoute } from '@/routes/product.route'; import { ProductRoute } from '@/routes/product.route';
import cors from 'cors'; import cors from 'cors';
import { CategoryRoute } from '@/routes/category.route';
const app = new App({ const app = new App({
port: 8000, port: 8000,
middlewares: [logger(), cors()], middlewares: [logger(), cors()],
controllers: [new ArticleRoute(), new ProductRoute()], controllers: [new ArticleRoute(), new ProductRoute(), new CategoryRoute()],
}); });
app.listen(); app.listen();
export interface ICategory {
id: number;
title: string;
description?: string;
}
import { Repository } from 'typeorm';
import { Category } from '@/entities/category.entity';
import { appDataSource } from '@/config/dataSource';
import { CategoryDto } from '@/dto/category.dto';
import { ICategory } from '@/interfaces/ICategory.interface';
export class CategoryRepository extends Repository<Category> {
constructor() {
super(Category, appDataSource.createEntityManager());
}
async getCategories(): Promise<ICategory[]> {
return await this.find();
}
async createCategory(categoryDto: CategoryDto): Promise<ICategory> {
return await this.save(categoryDto);
}
}
import { IProduct } from '@/interfaces/IProduct.interface'; import { IProduct } from '@/interfaces/IProduct.interface';
import { ProductDto } from '@/dto/ProductDto'; import { ProductDto } from '@/dto/product.dto';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { appDataSource } from '@/config/dataSource'; import { appDataSource } from '@/config/dataSource';
import { Product } from '@/entities/product.entity'; import { Product } from '@/entities/product.entity';
...@@ -9,7 +9,7 @@ export class ProductRepository extends Repository<Product> { ...@@ -9,7 +9,7 @@ export class ProductRepository extends Repository<Product> {
super(Product, appDataSource.createEntityManager()); super(Product, appDataSource.createEntityManager());
} }
async getProducts(): Promise<IProduct[]> { async getProducts(): Promise<IProduct[]> {
return await this.find(); return await this.find({ relations: { category: true } });
} }
async getProduct(id: number): Promise<IProduct> { async getProduct(id: number): Promise<IProduct> {
...@@ -24,14 +24,6 @@ export class ProductRepository extends Repository<Product> { ...@@ -24,14 +24,6 @@ export class ProductRepository extends Repository<Product> {
} }
async createProduct(productDto: ProductDto): Promise<IProduct> { async createProduct(productDto: ProductDto): Promise<IProduct> {
const { title, description, price, image } = productDto; return await this.save(productDto);
const product = new Product();
product.image = image;
product.title = title;
product.price = price;
product.description = description;
await this.save(product);
return product;
} }
} }
import { IRoute } from '@/interfaces/IRoute.interface';
import { Router } from 'express';
import { CategoryController } from '@/controllers/category.controller';
export class CategoryRoute implements IRoute {
public path = '/categories';
public router = Router();
private controller: CategoryController;
constructor() {
this.controller = new CategoryController();
this.init();
}
private init() {
this.router.get('/', this.controller.getCategories);
this.router.post('/', this.controller.createCategory);
}
}
import { CategoryRepository } from '@/repositories/category.repository';
import { ICategory } from '@/interfaces/ICategory.interface';
import { CategoryDto } from '@/dto/category.dto';
export class CategoryService {
private repository: CategoryRepository;
constructor() {
this.repository = new CategoryRepository();
}
getCategories = async (): Promise<ICategory[]> => {
return await this.repository.getCategories();
};
createCategory = async (categoryDto: CategoryDto): Promise<ICategory> => {
return await this.repository.createCategory(categoryDto);
};
}
import { IProduct } from '@/interfaces/IProduct.interface'; import { IProduct } from '@/interfaces/IProduct.interface';
import { ProductDto } from '@/dto/ProductDto'; import { ProductDto } from '@/dto/product.dto';
import { ProductRepository } from '@/repositories/product.repository'; import { ProductRepository } from '@/repositories/product.repository';
import { validate } from 'class-validator';
import { formatErrors } from '@/helpers/formatErrors';
export class ProductService { export class ProductService {
private repository: ProductRepository; private repository: ProductRepository;
...@@ -20,6 +22,8 @@ export class ProductService { ...@@ -20,6 +22,8 @@ export class ProductService {
} }
public async createProduct(productDto: ProductDto): Promise<IProduct> { public async createProduct(productDto: ProductDto): Promise<IProduct> {
const errors = await validate(productDto, { whitelist: true, validationError: { target: false, value: false } });
if (errors.length) throw formatErrors(errors);
return await this.repository.createProduct(productDto); return await this.repository.createProduct(productDto);
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment