跳转到内容

使用 Prisma

用 Prisma 和 PostgreSQL 替换内存用户 repository。

本指南承接开始使用,用 Prisma 和 PostgreSQL 替换内存用户 repository。

完成本指南后,通过 /auth/sign-up 创建的用户会存储在持久化数据库中,而不是内存里。

  • 已完成开始使用的项目
  • 一个 PostgreSQL 数据库

安装 Prisma、Prisma Client、PostgreSQL 驱动适配器和 pg:

Terminal window
pnpm add -D [email protected] @types/pg
pnpm add @prisma/[email protected] @prisma/adapter-pg pg dotenv

在项目根目录运行 Prisma 初始化命令:

Terminal window
pnpm dlx [email protected] init --output ../generated/prisma

这会创建一个 prisma/schema.prisma 文件,并配置生成的 Prisma Client 输出路径。

把 DATABASE_URL 添加到已有的 .env 文件中:

.env
DATABASE_URL="postgresql://USER:PASSWORD@localhost:5432/DB_NAME?schema=public"

把 USER、PASSWORD 和 DB_NAME 替换成你的 PostgreSQL 用户名、密码和数据库名。

更新 prisma/schema.prisma:

prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../generated/prisma"
moduleFormat = "cjs"
}
datasource db {
provider = "postgresql"
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
password String
}

这个 User 模型和开始使用中的示例用户结构一致。brkpt-auth 并不要求这个具体的模型,你的适配器决定哪些字段用于 JWT 载荷、密码校验和安全的用户响应。

创建并应用初始迁移:

Terminal window
pnpm dlx [email protected] migrate dev --name init

然后生成 Prisma Client:

Terminal window
pnpm dlx [email protected] generate

创建以下文件:

  • 文件夹src/
    • 文件夹prisma/
      • prisma.module.ts
      • prisma.service.ts

创建 PrismaService

创建 prisma.service.ts,包装生成的 PrismaClient:

src/prisma/prisma.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../../generated/prisma/client';
@Injectable()
export class PrismaService extends PrismaClient {
constructor() {
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL as string,
});
super({ adapter });
}
}

创建 PrismaModule

创建 prisma.module.ts,导出 PrismaService 以便适配器可以注入它:

src/prisma/prisma.module.ts
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

在开始使用中,BrkptAuthModule 导入了 UserModule,让适配器可以注入 MemoryUserRepository。

现在把 UserModule 替换成 PrismaModule:

src/brkpt-auth/brkpt-auth.module.ts
import { UserModule } from '../user/user.module';
import { PrismaModule } from '../prisma/prisma.module';
@Module({
imports: [UserModule],
imports: [PrismaModule],
controllers: [...features.flatMap((f) => f.controllers)],
providers: [...features.flatMap((f) => f.providers)],
})
export class BrkptAuthModule {
// ...
}

brkpt-auth 的功能本身不需要改动,只需要把适配器从 MemoryUserRepository 改为使用 PrismaService。

更新 CoreAdapter

把 CoreAdapter 改为使用 PrismaService:

src/brkpt-auth/adapters/core.adapter.ts
import { Injectable } from '@nestjs/common';
import { User } from '../../../generated/prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { CorePort } from '../features/core/core.port';
import { AuthJwtPayload } from './types';
@Injectable()
export class CoreAdapter implements CorePort<User> {
constructor(private readonly prisma: PrismaService) {}
mapUserToJwtPayload(user: User): AuthJwtPayload {
return {
sub: user.id,
email: user.email,
};
}
shrinkJwtPayload(payload: AuthJwtPayload): Record<string, unknown> {
return {
sub: payload.sub,
};
}
findUserByJwtPayload(payload: AuthJwtPayload): Promise<User | null> {
return this.prisma.user.findUnique({
where: {
id: payload.sub,
},
});
}
toSafeUser(user: User): Record<string, unknown> {
const { password: _password, ...safe } = user;
return safe;
}
extractUserIdFromJwtPayload(payload: AuthJwtPayload): number {
return payload.sub;
}
}

更新 CredentialsAdapter

把 CredentialsAdapter 改为使用 PrismaService:

src/brkpt-auth/adapters/credentials.adapter.ts
import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcrypt';
import { User } from '../../../generated/prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { CredentialsPort } from '../features/credentials/credentials.port';
import { SignInDto } from '../features/credentials/dto/sign-in.dto';
import { SignUpDto } from '../features/credentials/dto/sign-up.dto';
@Injectable()
export class CredentialsAdapter implements CredentialsPort<User> {
constructor(private readonly prisma: PrismaService) {}
findUserByDto(dto: SignInDto | SignUpDto): Promise<User | null> {
return this.prisma.user.findUnique({
where: {
email: dto.email,
},
});
}
validatePassword(user: User, dto: SignInDto): Promise<boolean> {
return bcrypt.compare(dto.password, user.password);
}
async createUser(dto: SignUpDto): Promise<User> {
const password = await bcrypt.hash(dto.password, 10);
return this.prisma.user.create({
data: {
name: dto.name,
email: dto.email,
password,
},
});
}
extractUserIdFromUser(user: User): number {
return user.id;
}
}

现在可以移除开始使用中创建的 src/user/ 文件夹:

  • 文件夹src/
    • 文件夹user/
      • 文件夹repositories/
        • memory-user.repository.ts
      • user.entity.ts
      • user.module.ts

这个文件夹的唯一作用就是为内存示例提供 MemoryUserRepository。把适配器切换到 PrismaService 之后,它就不再被使用了。

启动应用:

Terminal window
pnpm start:dev

通过 /auth/sign-up 创建一个用户,重启服务,再用同一个账号登录。

登录请求应该仍然能成功,因为用户现在存储在 PostgreSQL 中。