001 Работа с переменными окружения
Установим средство NestJS для работы с конфигом
npm i @nestjs/config
Далее добавим зависимость ConfigModule.forRoot()
в корневой модуль, которая позволит нам во всём проекте использовать один и тот же файл конфигурации (глобализирует модуль forRoot
). Она позволит нам работать с переменными окружения.
src / app.module.ts
@Module({
imports: [ConfigModule.forRoot(), AuthModule, TopPageModule, ProductModule, ReviewModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Записываем какое-либо значение в переменные окружения.
.env
TEST=1
И уже в любом другом нужном нам модуле мы можем заинжектить этот сервис по работе с переменными окружения и воспользоваться его функционалом: получить значение переменной можно с помощью функции get()
, которая принимает в себя наименование переменной.
src / top-page / top-page.controller.ts
@Controller('top-page')
export class TopPageController {
constructor(private readonly configService: ConfigService) {}
@Get(':id')
async get(@Param('id') id: string) {
return this.configService.get('TEST');
}
/// CODE ...
}
002 Подготовка окружения
Первым делом нужно установить на ПК Docker
После установки докера, нужно настроить его окружение под наш проект:
version
- минимальная версия докераservices
- сервисы, которые используются в данном контейнереimage
- образ, который запускает контейнерcontainer_name
- имя контейнераrestart
- перезапуск каждый раз, когда у нас перезагрузился серверenvironment
- переменные окружения (логин и пароль администратора)ports
- позволяет прокинуть порт изнутри контейнера наружу (без них не получится подключиться к БД)volumes
- позволяет подключить часть дискового пространства внутри контейнера к диску на сервере (компьютере)command
- команды (ограничение кеша для БД)
docker-compose.yml
version: '3'
services:
mongo:
image: mongo:4.4.4
container_name: mongo
restart: always
environment:
- MONGO_INITDB_ROOT_USERNAME=admin
- MONGO_INITDB_ROOT_PASSWORD=admin
ports:
- 27017:27017
volumes:
- ./mongo-data-4.4:/data/db
command: --wiredTigerCacheSizeGB 1.5
Позволяет поднять контейнер по заданным параметрам (по файлу docker-compose.yml
в папке, где запускается команда):
docker-compose up -d
Позволяет посмотреть контейнеры | выбираем отдельный контейнер монги:
docker ps | grep mongo
Примечание: grep
работает только на unix-системах
Так же посмотреть на статус контейнера можно через десктопное приложение докера
Остановить и запустить контейнер мы можем следующими комадами:
docker stop <имя_контейнера>
docker start <имя_контейнера>
003 Подключение Mongo
Сейчас нам нужно установить данные модули:
mongoose
- удобная ORM для монгиtypegoose
- позволяет проще описать модели дляmongoose
nestjs-typegoose
- позволяет использоватьtypegoose
в несте более нативно (приближенно к основным подходам фреймворка)
// тут может потребоваться --legacy-peer-deps
npm i @typegoose/typegoose mongoose nestjs-typegoose
npm i -D @types/mongoose
Записываем переменные для подключения к монге в окружение
.env
MONGO_LOGIN=admin
MONGO_PASSWORD=admin
MONGO_HOST=localhost
MONGO_PORT=27017
MONGO_AUTHDATABASE=admin
Далее, в основном модуле приложения, подключим модуль провайдера монги TypegooseModule
Тут мы используем вместо forRoot
метод forRootAsync
, который позволит асинхронно инициализировать модуль вместе с его зависимостями. Это делается для того, чтобы иметь возможность использовать ConfigModule
в зависимостях модуля тайпгуза
Для того, чтобы использовать любой провайдер, нужно использовать модуль, который содержит этот провайдер, поэтому вставляем в import
модуль ConfigModule
В inject
мы вставляем зависимость из ConfigModule
, а именно тут это представляет ConfigService
, который позволит нам получить данные из .env
Внутрь useFactory
будет помещать функцию, которая сгенерирует строку подключения к БД монги getMongoConfig
.
src / app.module.ts
@Module({
imports: [
ConfigModule.forRoot(),
// асинхронно подключаем конфигурацию
TypegooseModule.forRootAsync({
// тут мы импортируем модули провайдеров
imports: [ConfigModule],
// тут мы вставляем зависимость в фэктори из модуля, который в него импортировали
inject: [ConfigService],
// сюда мы передаём конфиг подключения к монге
useFactory: getMongoConfig,
}),
AuthModule,
TopPageModule,
ProductModule,
ReviewModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Сейчас нам нужно написать саму функцию getMongoConfig
, которая сгенерирует параметры подключения.
Таким образом должен выглядеть объект подключения по типам:
Все конфиги, которые мы передаём в модули, лучше складировать в папку configs
.
getMongoConfig
- эта функция возвращает объект, который содержит в себе строку подключения к монге и деструктурированный объект, возвращаемый из функции, которая возвращает опции монгиgetMongoString
- возвращает строку подключения к монге через обращение кconfigService
getMongoOptions
- возвращает опции для подключения к монге
src / configs / mongo.config.ts
import { ConfigService } from '@nestjs/config';
import { TypegooseModuleOptions } from 'nestjs-typegoose';
// получаем строку подключения к монге
const getMongoString = (configService: ConfigService) =>
'mongodb://' +
configService.get('MONGO_LOGIN') +
':' +
configService.get('MONGO_PASSWORD') +
'@' +
configService.get('MONGO_HOST') + // хост
':' +
configService.get('MONGO_PORT') + // порт
'/' +
configService.get('MONGO_AUTHDATABASE'); // база, к которой подключаемся
// получение опций для подключения к монге
const getMongoOptions = () => ({ });
// это функция получения конфига для подключения к монге
export const getMongoConfig = async (
configService: ConfigService,
): Promise<TypegooseModuleOptions> => {
// возвращаем объект, который вызывает две функции, которые вернут нам - строку и опции для подключения
return {
uri: getMongoString(configService),
...getMongoOptions(),
};
};
И теперь npm start
запустит наш сервер с подключением к монге
004 Подключение моделей
Далее нам нужно подготовить наши модели данных и навесить на них декораторы, чтобы typegoose
понял, как работать с этими данными
В модуль нужно импортировать TypegooseModule
, из которого буде использовать локальную функциональность (forFeature
), в которой опишем модель данных, которую будет иметь данный модуль
src > auth > auth.module.ts
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { TypegooseModule } from 'nestjs-typegoose';
import { AuthModel } from './auth.model';
@Module({
controllers: [AuthController],
// добавляем импорт модуля
imports: [
// подключаем локально для модуля модели
TypegooseModule.forFeature([
{
typegooseClass: AuthModel, // класс модели
// опции схемы данных
schemaOptions: {
collection: 'Auth', // имя коллекции
},
},
]),
],
})
export class AuthModule {}
Далее нам нужно описать свойства (а именно, их характеристики), которые будут попадать в монгу через декораторы
@prop
- описывает данные как отдельные свойства, которые будут класться в базу. Данный декоратор стоит добавлять на все свойства, которые мы отправляем в монгу.
Ну и так же далее нам нужно сделать TimeStamps, который будет помечать время создания объекта. Можно создать его отдельным свойством (createdAt
), а можно сделать правильно и расширить ДТОшку от тайпгуза через extends TimeStamp
Вместе с этим нужно добавить ещё и _id
в нашу модель. Это можно сделать ещё более красивым способом - заэкстендить интерфейс с именем класса модели данных от класса Base
. И тут нужно будет сказать, что в наши модели данных теперь нельзя добавлять поле _id
, так как оно присутствует в Base
. Сам интерфейс обязательно нужно экспортировать, чтобы TS смог смёрджить класс и интерфейс
src > auth > auth.model.ts
import { prop } from '@typegoose/typegoose';
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses';
export interface AuthModel extends Base {}
export class AuthModel extends TimeStamps {
@prop({
// index: true, // это поле будет являться индексом в БД
unique: true, // сделает поле уникальным и так же индексом записи
})
email: string;
@prop()
passwordHash: string;
}
Чтобы пустые интерфейсы не подсвечивались, нужно их оффнуть в еслинте
Добавляем ровно то же самое подключение Typegoose
модуля для подключение модели к модулю продукта
src > product > product.module.ts
import { Module } from '@nestjs/common';
import { ProductController } from './product.controller';
import { TypegooseModule } from 'nestjs-typegoose';
import { AuthModel } from '../auth/auth.model';
@Module({
controllers: [ProductController],
imports: [
TypegooseModule.forFeature([
{
typegooseClass: ProductModel,
schemaOptions: {
collection: 'Product',
},
},
]),
],
})
export class ProductModule {}
Если мы хотим в @prop
указать тип, то нам придётся указать такую конструкцию:
Между []
попадает не тип TypeScript
, а конструктор типа Typegoose
@prop({ type: () => [String] })
tags: string[];
Типизация модели продукта:
src > product > product.model.ts
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses';
import { prop } from '@typegoose/typegoose';
class ProductCharacteristic {
@prop()
name: string;
@prop()
value: string;
}
export interface ProductModel extends Base {}
export class ProductModel extends TimeStamps {
@prop()
image: string;
@prop()
title: string;
@prop()
price: number;
@prop()
oldPrice: number;
@prop()
credit: number;
@prop()
calculatedRating: number;
@prop()
description: string;
@prop()
advantages: string;
@prop()
disAdvantages: string;
@prop({ type: () => [String] })
categories: string[];
@prop({ type: () => [String] })
tags: string[];
@prop({
type: () => [ProductCharacteristic], // типизируем запрос
_id: false, // отключаем автоматическую генерацию id в массиве
})
characteristics: ProductCharacteristic[];
}
Подключение модуля тайпгуза к обзору:
src > review > review.module.ts
import { Module } from '@nestjs/common';
import { ReviewController } from './review.controller';
import { ReviewModel } from './review.model';
import { TypegooseModule } from 'nestjs-typegoose';
@Module({
controllers: [ReviewController],
imports: [
TypegooseModule.forFeature([
{
typegooseClass: ReviewModel,
schemaOptions: {
collection: 'Review',
},
},
]),
],
})
export class ReviewModule {}
Типизация модели обзора:
src > review > review.model.ts
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses';
import { prop } from '@typegoose/typegoose';
export interface ReviewModel extends Base {}
export class ReviewModel extends TimeStamps {
@prop()
name: string;
@prop()
title: string;
@prop()
description: string;
@prop()
rating: number;
// удаляем, так как экстендим от TimeStamps
// @prop()
// createdAt: Date;
}
Подключение модуля главной страницы:
src > top-page > top-page.module.ts
import { Module } from '@nestjs/common';
import { TopPageController } from './top-page.controller';
import { TypegooseModule } from 'nestjs-typegoose';
import { TopPageModel } from './top-page.model';
@Module({
controllers: [TopPageController],
imports: [
TypegooseModule.forFeature([
{
typegooseClass: TopPageModel,
schemaOptions: {
collection: 'TopPage',
},
},
]),
],
})
export class TopPageModule {}
Типизация модели главной страницы:
src > top-page > top-page.model.ts
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses';
import { prop } from '@typegoose/typegoose';
export enum TopLevelCategory {
Courses,
Services,
Books,
Products,
}
export class HhData {
@prop()
count: number; // количество вакансий
@prop()
juniorSalary: number;
@prop()
middleSalary: number;
@prop()
seniorSalary: number;
}
export class TopPageAdvantages {
@prop()
title: string;
@prop()
description: string;
}
export interface TopPageModel extends Base {}
export class TopPageModel extends TimeStamps {
@prop({ enum: TopLevelCategory })
firstCategory: TopLevelCategory;
@prop()
secondCategory: string;
@prop({ unique: true })
alias: string;
@prop()
title: string;
@prop()
category: string;
@prop({ type: () => HhData }) // возвращается тип модели HhData
hh?: HhData;
@prop({ type: () => [TopPageAdvantages] })
advantages: TopPageAdvantages[];
@prop()
seoText: string;
@prop()
tagsTitle: string;
@prop({ type: () => [String] })
tags: string[];
}
Если бы мы присвоили значения для свойства енама, то можно было бы указать и возвращаемый тип
И теперь тут отображены после кор-модуля все 4 модуля forFeature
, которые мы подключили к модулям приложения
005 Сервис отзывов
Добавляем новый сервис через CLI неста
nest g service review
Далее добавляется в модуль зависимость от данного сервиса в провайдерах
Далее переходим в сервис и первым делом в конструкторе инжектим ReviewModel
, который предоставит доступ к редактированию данных внутри данной модели (предоставит доступ к методам изменения, создания и так далее)
src > review > review.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { ReviewModel } from './review.model';
import { ModelType } from '@typegoose/typegoose/lib/types';
@Injectable()
export class ReviewService {
// инжектим модель в сервис
constructor(@InjectModel(ReviewModel) private readonly reviewModel: ModelType<ReviewModel>) {}
}
Так же нам нужно добавить в модель обзора id
продукта, к которому будет присвоен комментарий
src > review > review.model.ts
Далее нам нужно описать ДТОшку модели данных, которую нужно соблюдать для создании нового обзора на продукт
src > review > dto > create-review.dto.ts
export class CreateReviewDto {
name: string;
title: string;
description: string;
rating: number;
productId: string;
}
И далее подставим для контроллера создания новую ДТОшку, которая будет отвечать за модель принимаемых данных для добавления обзора
src > review > review.controller.ts
Приступаем к описанию сервиса, который будет напрямую взаимодействовать с провайдером (хранить методы для модификации и изменения данных):
create
- метод создания нового обзораfindByProductId
- метод поиска обзора по продукту (для вывода обзоров по продукту)delete
- удаление обзораdeleteByProductId
- удаление обзоров поid
продукта (например, если удалится продукт, то вместе с ним и обзор)
Конкретно reviewModel
предоставляет нам методы для работы с данными внутри Mongo
:
find
- найти запись по query, которая принимает в себя объект с полями{}
create
- создание нового объекта по модели данныхfindByIdAndDelete
- поиск и удаление записи поid
deleteMany
- удаление нескольких записей по query
В некоторых операциях используется метод exec
, который запускает операцию
Когда мы пишем в query productId: new Types.ObjectId(productId)
, то тут мы вызываем поиск по новому созданному типу, который является уникальным идентификатором для записи
src > review > review.service.ts
import { Injectable } from '@nestjs/common';
import { ReviewModel } from './review.model';
import { ModelType, DocumentType } from '@typegoose/typegoose/lib/types';
import { CreateReviewDto } from './dto/create-review.dto';
import { Types } from 'mongoose';
import { InjectModel } from 'nestjs-typegoose';
@Injectable()
export class ReviewService {
// инжектим модель, которая содержит методы mongoose
constructor(
@InjectModel(ReviewModel)
private readonly reviewModel: ModelType<ReviewModel>
) {}
// метод создания нового обзора по продукту
async create(dto: CreateReviewDto): Promise<DocumentType<ReviewModel>> {
// возвращает созданный обзор
return this.reviewModel.create(dto);
}
// метод удаления обзора
async delete(id: string): Promise<DocumentType<ReviewModel> | null> {
// findByIdAndDelete - найти по id и удалить
// exec - запрашивает выполнение данной операции
return this.reviewModel.findByIdAndDelete(id).exec();
}
// метод удаления всех обзоров по продукту (нужно, если удаляем продукт)
async deleteByProductId(productId: string) {
return this.reviewModel.deleteMany({
productId: new Types.ObjectId(productId)
}).exec();
}
// метод поиска обзора по продукту (если переходим на просмотр продукта)
async findByProduct(
productId: string
): Promise<DocumentType<ReviewModel>[]> {
return this.reviewModel.find({
productId: new Types.ObjectId(productId)
}).exec();
}
}
И далее в контроллере (который принимает в себя запросы с фронта) вызываем методы сервиса, который уже и производит изменения в базе данных
Сюда в конструктор вставляем ReviewService
из review.service.ts
.
Далее нам остаётся только добавить методы из сервиса ReviewService
.
Наименования методов ровно такие же, как и в сервисе.
Сам контроллер работает только с http
-запросами. Ответы на ошибки приходят тоже из него (тот же нестовский HttpException
, который отправляет на фронт ответ со сгенерированной ошибкой)
src > review > review.controller.ts
import { Controller, Delete, Get, HttpException, HttpStatus, Param, Post } from '@nestjs/common';
import { Body } from '@nestjs/common/decorators';
import { CreateReviewDto } from './dto/create-review.dto';
import { ReviewService } from './review.service';
import { REVIEW_NOT_FOUND } from './review.constants';
@Controller('review')
export class ReviewController {
// тут мы принимаем инстанс сервиса обзоров
constructor(private readonly reviewService: ReviewService) {}
@Post('create')
async create(@Body() dto: CreateReviewDto) {
return this.reviewService.create(dto); // создаём новый DTO
}
@Delete(':id')
async delete(@Param('id') id: string) {
// удаляем нужную запись получаем удалённый документ
const deletedDoc = await this.reviewService.delete(id);
// если мы ничего не удалили, то
if (!deletedDoc) {
// выкенем нестовскую http-ошибку
// 1арг - строка с сообщением ошибки, 2арг - статус ошибки
throw new HttpException(REVIEW_NOT_FOUND, HttpStatus.NOT_FOUND);
}
}
@Get('getByProduct/:productId')
async getByProduct(@Param('productId') productId: string) {
return this.reviewService.findByProductId(productId);
}
}
Тут же мы храним константы, которые используются в модуле. Конкретно здесь хранится текстовый ответ, который придёт на фронт с ошибкой
src > review > review.constants.ts
export const REVIEW_NOT_FOUND = 'Отзыв по такому id не найден';