База данных с Prisma ORM, Docker и Postgres — NestJs с Passport #02

В прошлом посте мы начали с пустой конфигурации, поняли, как NestJs работает с маршрутами, контроллерами и сервисами. Увидели, как легко настроить Fastify для оптимизации нашего приложения.

Теперь мы настроим базу данных и ORM для взаимодействия и хранения наших данных. Для базы данных мы используем PostgreSQL, используя docker для создания контейнера по умолчанию для приложения, для ORM мы будем использовать Prisma, потому что это лучший Orm на данный момент для взаимодействия с базой данных.


Контейнер Docker

Теперь, когда мы создали наше приложение, давайте его контейнеризируем.

Начните с создания следующих файлов в корневом каталоге проекта:

  • Dockerfile — Этот файл будет отвечать за импорт образов Docker, разделение их на среду разработки и среду производства, копирование всех наших файлов и установку зависимостей.

  • docker-compose.yml — Этот файл будет отвечать за определение наших контейнеров, необходимых образов для приложений, других сервисов, томов хранения, переменных окружения и т.д.

Откройте Dockerfile и добавьте

# Dockerfile

FROM node:alpine As development

WORKDIR /usr/src/app

COPY package*.json ./

RUN yarn add glob rimraf

RUN yarn --only=development

COPY . .

RUN yarn build

FROM node:alpine as production

ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}

WORKDIR /usr/src/app

COPY package*.json ./

RUN yarn add glob rimraf

RUN yarn --only=production

COPY . .

COPY --from=development /usr/src/app/dist ./dist

CMD ["node", "dist/main"]
Вход в полноэкранный режим Выйти из полноэкранного режима

Откройте файл docker-compose.yml и добавьте следующий код

# docker-compose.yml

version: "3.7"

services:
  main:
    container_name: main
    build:
      context: .
      target: development
    volumes:
      - .:/usr/src/app
      - /usr/src/app/node_modules
    ports:
      - 3000:3000
    command: yarn start:dev
    env_file:
      - .env
    networks:
      - api
    depends_on:
      - postgres
  postgres:
    image: postgres:13
    container_name: postgres
    networks:
      - api
    env_file:
      - .env
    ports:
      - 5432:5432
    volumes:
      - pgdata:/var/lib/postgresql/data
networks:
  api:
volumes:
  pgdata:
Ввести полноэкранный режим Выйти из полноэкранного режима

Создайте файл .env и добавьте учетные данные PostgreSQL

# .env

# PostgreSQL
POSTGRES_USER=nestAuth
POSTGRES_PASSWORD=nestAuth
POSTGRES_DB=nestAuth
Войти в полноэкранный режим Выход из полноэкранного режима

По умолчанию Fastify слушает только интерфейс localhost 127.0.0.1. Для доступа к нашему приложению с других хостов необходимо добавить 0.0.0.0.0 в main.ts.

// src/main.ts

await app.listen(3000, "0.0.0.0");
Вход в полноэкранный режим Выйти из полноэкранного режима

Отлично, у нас есть наш докер, и давайте приступим к тестированию. Запустите в терминале для разработки

docker-compose up
Войти в полноэкранный режим Выйти из полноэкранного режима

И наше приложение запущено ?.


Prisma ORM

Prisma — это ORM с открытым исходным кодом, он используется как альтернатива написанию простого SQL или использованию других инструментов доступа к базе данных, таких как конструкторы SQL запросов (например, knex.js) или ORM (например, TypeORM и Sequelize).

Начните установку Prisma CLI в качестве разработки

yarn add Prisma -D
Войдите в полноэкранный режим Выйти из полноэкранного режима

В качестве лучшей практики вызывайте CLI локально, используя префикс npx, чтобы создать начальную установку Prisma с помощью команды init.

npx prisma init
Вход в полноэкранный режим Выйти из полноэкранного режима

Эта команда создает новый каталог Prisma со следующим содержимым

По умолчанию подключение к базе данных установлено на postgresql.

// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Если тип нашего соединения правильный, мы установим DATABASE_URL в .env.

DATABASE_URL="postgresql://nestAuth:nestAuth@postgres:5432/nestAuth"
Вход в полноэкранный режим Выйти из полноэкранного режима

Не забудьте добавить в .env в .gitignore и создать .env.example перед созданием репозитория на Github

Для генерации Prisma Client требуется файл schema.prisma. COPY prisma ./prisma/ копирует весь каталог Prisma на случай, если вам также понадобятся миграции.

# Dockerfile

FROM node:alpine As development

WORKDIR /usr/src/app

COPY package*.json ./

# Here Prisma folder to the container
COPY prisma ./prisma/

RUN yarn add glob rimraf

RUN yarn --only=development

COPY . .

RUN yarn build

FROM node:alpine as production

ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}

WORKDIR /usr/src/app

COPY package*.json ./

# Here Prisma folder to the container
COPY prisma ./prisma/

RUN yarn add glob rimraf

RUN yarn --only=production

COPY . .

COPY --from=development /usr/src/app/dist ./dist

CMD ["node", "dist/main"]
Вход в полноэкранный режим Выход из полноэкранного режима

Первая модель

Теперь для проверки соединения мы создадим модель User, внутри schema.prisma вставка

// prisma/schema.prisma

model User {
  id        Int            @id @default(autoincrement())
  email     String         @unique
  name      String
  password  String
  createdAt DateTime       @default(now())
  updatedAt DateTime       @updatedAt
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Когда модель создана, вы можете сгенерировать файлы миграции SQL и запустить их в базе данных. Здесь я использую migrate dev для запуска в режиме разработки и задаю имя init для миграции.

Перед этим вам нужно настроить ваш docker

docker-compose up
Войти в полноэкранный режим выйти из полноэкранного режима

и отредактировать файл .env.

DATABASE_URL="postgresql://nestAuth:nestAuth@localhsot:5432/nestAuth"
Войдите в полноэкранный режим Выйти из полноэкранного режима

Всегда, когда вы запускаете prisma migrate, вам нужно изменить хост базы данных на localhost после отката к имени вашего контейнера базы данных.

npx prisma migrate dev --name init
Вход в полноэкранный режим Выход из полноэкранного режима

Откат файла .env

DATABASE_URL="postgresql://nestAuth:nestAuth@nestauth:5432/nestAuth"
Войти в полноэкранный режим Выйти из полноэкранного режима

Хорошие новости, наша конфигурация работает, теперь наша база данных синхронизирована с нашим приложением ?.


Настройка Prisma

Мы хотим абстрагироваться от Prisma Client API для запросов к базе данных в рамках сервиса. Поэтому давайте создадим новый PrismaService, который позаботится об инстанцировании и PrismaClient для подключения к вашей базе данных.

Создайте prisma.service.ts внутри папки src.

// src/prisma.service.ts

import { INestApplication, Injectable, OnModuleInit } from "@nestjs/common";
import { PrismaClient } from "@prisma/client";

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect();
  }

  async enableShutdownHooks(app: INestApplication) {
    this.$on("beforeExit", async () => {
      await app.close();
    });
  }
}
Войдите в полноэкранный режим Выйти из полноэкранного режима

Первый сервис

Теперь мы можем написать пользовательский сервис для выполнения вызовов базы данных. В NestJs CLI есть команда nest g для генерации сервисов, контроллеров, стратегий и других структур. На данный момент мы запускаем

nest g service users
Войти в полноэкранный режим Выйти из полноэкранного режима

Перед началом создания сервиса нам нужно сгенерировать типы Prisma Model, мы можем сгенерировать с помощью этого

npx prisma generate
Войти в полноэкранный режим Выйти из полноэкранного режима

Внутри папки src команда создает папку users с файлом users.service.ts и файлом test users.service.spec.ts. Чтобы протестировать наше подключение к базе данных, создадим два сервиса

И внутри createUser нам нужно зашифровать пароль пользователя, поэтому создадим для этого провайдера, в папке src создадим папку providers и создадим файл password.ts.

// src/providers/password.ts

import { Injectable } from "@nestjs/common";
import * as bcrypt from "bcrypt";

const SALT_OR_ROUNDS = 10;

@Injectable()
export class PasswordProvider {
  async hashPassword(password: string): Promise<string> {
    return bcrypt.hashSync(password, SALT_OR_ROUNDS);
  }

  async comparePassword(password: string, hash: string): Promise<boolean> {
    return bcrypt.compareSync(password, hash);
  }
}
Войдите в полноэкранный режим Выйти из полноэкранного режима

Класс имеет два метода, hashPassword и comparePassword для шифрования и сравнения пароля с помощью brcypt. Внутри класса UsersService необходимо добавить в конструктор провайдер PasswordProvider для использования в методах.

// src/users/users.service.ts

import { HttpException, HttpStatus, Injectable } from "@nestjs/common";
import { PrismaService } from "../prisma.service";
import { User, Prisma } from "@prisma/client";
import { PasswordProvider } from "src/providers/password";

@Injectable()
export class UsersService {
  constructor(
    private prisma: PrismaService,
    private passwordProvider: PasswordProvider
  ) {}

  async user(
    userWhereUniqueInput: Prisma.UserWhereUniqueInput
  ): Promise<User | null> {
    const user = await this.prisma.user.findUnique({
      where: userWhereUniqueInput,
    });

    delete user.password;

    return user;
  }

  async createUser(data: Prisma.UserCreateInput): Promise<User> {
    const userExists = await this.prisma.user.findUnique({
      where: { email: data.email },
    });

    if (userExists) {
      throw new HttpException("User already exists", HttpStatus.CONFLICT);
    }

    const passwordHashed = await this.passwordProvider.hashPassword(
      data.password
    );

    const user = await this.prisma.user.create({
      data: {
        ...data,
        password: passwordHashed,
      },
    });

    delete user.password;

    return user;
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

С созданным сервисом давайте создадим контроллер для использования маршрута, который

nest g controller users
Вход в полноэкранный режим Выход из полноэкранного режима

Эта команда создает users.controller.ts и наш тестовый файл внутри src/users, поэтому давайте создадим две функции в контроллере

// src/users/users.controller.ts

import { Body, Controller, Get, Param, Post } from "@nestjs/common";
import { User } from "@prisma/client";
import { UsersService } from "./users.service";

// Set prefix route for this group. Ex.: for get profile /users/8126321
@Controller("users")
export class UsersController {
  constructor(private readonly usersService: UsersService) {}
  // Create user -> POST /users
  @Post()
  async signupUser(
    @Body() userData: { name: string; email: string; password: string }
  ): Promise<User> {
    return this.usersService.createUser(userData);
  }

  // Get user Profile -> GET /users/:id
  @Get("/:id")
  async profile(@Param("id") id: number): Promise<User> {
    return this.usersService.user({ id: Number(id) });
  }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Внутри файла users.module.ts нам нужно добавить провайдеры, экспорты и массив контроллеров.

// src/users/users.module.ts

import { Module } from "@nestjs/common";
import { PrismaService } from "src/prisma.service";
import { PasswordProvider } from "src/providers/password";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";

@Module({
  providers: [PasswordProvider, UsersService, PrismaService],
  exports: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}
Вход в полноэкранный режим Выйти из полноэкранного режима

И передайте UsersModule в AppModule для использования.

//src/app.module.ts

import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { PrismaService } from "./prisma.service";
import { UsersModule } from "./users/users.module";
import { UsersService } from "./users/users.service";
import { PasswordProvider } from "./providers/password";

@Module({
  imports: [UsersModule],
  controllers: [AppController],
  providers: [PrismaService, UsersService, PasswordProvider],
})
export class AppModule {}
Вход в полноэкранный режим Выход из полноэкранного режима

Давайте протестируем

Теперь запустим наш контейнер docker

docker-compose up
Войти в полноэкранный режим Выйти из полноэкранного режима

Вот и все! Приложение запущено ?

Итак, в Postman давайте попробуем использовать маршруты createUser и getProfile.

curl --location --request POST 'http://0.0.0.0:3000/users' 
--header 'Content-Type: application/json' 
--data-raw '{
    "email": "test@e3x.com",
    "name": "Gabriel Menezes",
    "password": "123123"
}'
Вход в полноэкранный режим Выход из полноэкранного режима

curl --location --request GET 'http://0.0.0.0:3000/users/37'
Войти в полноэкранный режим Выход из полноэкранного режима


До следующего раза

Это все, что мы рассмотрим в этой статье: мы докеризировали приложение, настроили Prisma и создали два маршрута. В следующей части цикла мы создадим и определим Auth-провайдеры для аутентификации в нашем приложении.

Спасибо за чтение!


Перейдите в репозиторий для ознакомления с кодом

mnzsss / nest-auth-explained

? Аутентификация с помощью JWT и Google в Nest.js объяснено


Ссылки

  • Настройка проекта NestJS с помощью Docker для Back-End разработки
  • Докеризация приложения NestJS с Prisma и PostgreSQL

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *