-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
精通 NestJS – Node.js 框架 Master NestJS 9 - Node.js Framework #187
Comments
NestJS 简介安装使用 Nest CLInpm i -g @nestjs/cli
nest --help 使用模板库https://github.com/nestjs/typescript-starter git clone https://github.com/nestjs/typescript-starter.git project
cd project
npm install
npm run start:dev 在windows上,本地开发,修改代码,热更新后,会出现3000端口被占用的错误。需要结束node taskkill /F /IM node.exe 创建新的nest项目项目名为 nest-events-backend 启动开发服务器 cd nest-events-backend
npm run start:dev 通过 localhost:3000 访问后端服务 003 NestJS Project Structure NestJS 项目结构Nest应用程序由模块组成,默认情况下总是至少有一个主模块,称为应用程序模块。 主入口函数 src\main.tsbootstrap函数负责创建新的nest应用程序对象 ModuleModule 模块是一个抽象的概念。包含一组功能。 import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {} Controller控制器定义API和端点 Service服务是一个类,应用程序的业务逻辑放在其中, app.controller.spec.ts控制器测试放在控制器旁边 |
03 - Controllers, Routing, Requests 控制器、路由、请求简介路由基本上是定义了路径,URL,HTTP动词,状态码。 002 Controllers控制器是一个带有控制器装饰器注释的类。 你需要告诉应用程序如何将一个路径转化为控制器和它们的动作。
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller('/events')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
} 方式2: @Controller({
path: '/events',
}) 空路由表示根路由@controller() 空路由 / import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
} getHello() 表示根路径的请求响应 访问 路由import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@Get('/bye')
getbye(): string {
return 'Bye';
}
} @get('/bye') 003 Resource Controller 围绕资源组织控制器和请求动作创建事件资源控制器 EventsControllernest genarate controller
import { Controller, Delete, Get, Patch, Post } from '@nestjs/common';
@Controller('/events')
export class EventsController {
@Get()
findAll() {}
@Get()
findOne() {}
@Post()
create() {}
@Patch()
update() {}
@Delete()
remove() {}
} 向 nest 注册控制器 EventsController
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { EventsController } from './events.controller';
@Module({
imports: [],
controllers: [AppController, EventsController],
providers: [AppService],
})
export class AppModule {} 使用各请求动作访问 localhost:3000/events 围绕着资源建立的控制器,尽量保持简短,并且最多拥有五个基本的操作, 资源并不总是指我们的数据库表,它可以是更抽象的东西。 004 Route Parameters 路由参数通过参数装饰器 @param 将参数传递给请求 单独获取 id 参数 @Get(':id')
findOne(@Param('id') id) {
return id;
} 如果参数过多,可以获取所有参数 @Get(':id')
findOne(@Param() Params) {
return Params.id;
} import { Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';
@Controller('/events')
export class EventsController {
@Get()
findAll() {
return 'all';
}
@Get(':id')
findOne(@Param('id') id) {
return id;
}
@Post()
create() {}
@Patch(':id')
update(@Param('id') id) {}
@Delete(':id')
remove(@Param('id') id) {}
} 005 Request Body 请求体json请求体通过 @Post()
create(@Body() input) {
// json请求体通过@Body转换程js对象 input
return input;
}
import {
Controller,
Delete,
Get,
Param,
Patch,
Post,
Body,
} from '@nestjs/common';
@Controller('/events')
export class EventsController {
@Get()
findAll() {
return 'all';
}
@Get(':id')
findOne(@Param('id') id) {
return id;
}
@Post()
create(@Body() input) {
// json请求体通过@Body转换程js对象 input
return input;
}
@Patch(':id')
update(@Param('id') id, @Body() input) {}
@Delete(':id')
remove(@Param('id') id) {}
} 006 Responses and Status Codes 响应和状态码响应响应有两种情况 1-返回一个原始值,如字符串。数字或布尔,字面上的东西被返回 2-返回一个数组或一个对象,它被序列化为json
@Get()
findAll() {
return [
{ id: 1, name: 'First event' },
{ id: 2, name: 'Second event' },
];
} import {
Controller,
Delete,
Get,
Param,
Patch,
Post,
Body,
} from '@nestjs/common';
@Controller('/events')
export class EventsController {
@Get()
findAll() {
return [
{ id: 1, name: 'First event' },
{ id: 2, name: 'Second event' },
];
}
@Get(':id')
findOne(@Param('id') id) {
return { id: 1, name: 'First event' };
}
@Post()
create(@Body() input) {
// json请求体通过@Body转换程js对象 input
return input;
}
@Patch(':id')
update(@Param('id') id, @Body() input) {}
@Delete(':id')
remove(@Param('id') id) {}
} 响应 默认get成功的的响应状态码为200 创建资源成功状态码为201 @Post()
create(@Body() input) {
// json请求体通过@Body转换程js对象 input
return input;
} 更新资源成功状态码为200 良好的rest API实践是返回刚刚创建或更新的结果。 import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
} from '@nestjs/common';
@Controller('/events')
export class EventsController {
@Get()
findAll() {
return [
{ id: 1, name: 'First event' },
{ id: 2, name: 'Second event' },
];
}
@Get(':id')
findOne(@Param('id') id) {
return { id: 1, name: 'First event' };
}
@Post()
create(@Body() input) {
return input;
}
@Patch(':id')
update(@Param('id') id, @Body() input) {
return input;
}
@Delete(':id')
@HttpCode(204)
remove(@Param('id') id) {}
} 007 Request Payload - Data Transfer Objects 请求携带的有效负载-数据传输对象 DTO数据传输对象 DTO :定义请求体的属性和这个属性的类型 create-event.dto.ts
export class CreateEventDto {
name: string;
description: string;
when: string;
address: string;
} 将DTO类型声明添加到创建动作的输入参数中
import { CreateEventDto } from './create-event.dto';
@Post()
create(@Body() input: CreateEventDto) {
return input;
} import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
} from '@nestjs/common';
import { CreateEventDto } from './create-event.dto';
@Controller('/events')
export class EventsController {
@Get()
findAll() {
return [
{ id: 1, name: 'First event' },
{ id: 2, name: 'Second event' },
];
}
@Get(':id')
findOne(@Param('id') id) {
return { id: 1, name: 'First event' };
}
@Post()
create(@Body() input: CreateEventDto) {
return input;
}
@Patch(':id')
update(@Param('id') id, @Body() input) {
return input;
}
@Delete(':id')
@HttpCode(204)
remove(@Param('id') id) {}
} 008 The Update Payload 更新请求的有效载荷改变一个事件的一些属性,但不发送不需要改变的属性。 继承并将创建资源的DTO的属性改为可选类型 npm i -S @nestjs/mapped-types
import { PartialType } from '@nestjs/mapped-types';
import { CreateEventDto } from './create-event.dto';
export class UpdateEventDto extends PartialType(CreateEventDto) {}
@Patch(':id')
update(@Param('id') id, @Body() input: UpdateEventDto) {
return input;
} import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
} from '@nestjs/common';
import { CreateEventDto } from './create-event.dto';
import { UpdateEventDto } from './update-event.dto';
@Controller('/events')
export class EventsController {
@Get()
findAll() {
return [
{ id: 1, name: 'First event' },
{ id: 2, name: 'Second event' },
];
}
@Get(':id')
findOne(@Param('id') id) {
return { id: 1, name: 'First event' };
}
@Post()
create(@Body() input: CreateEventDto) {
return input;
}
@Patch(':id')
update(@Param('id') id, @Body() input: UpdateEventDto) {
return input;
}
@Delete(':id')
@HttpCode(204)
remove(@Param('id') id) {}
} 009 A Working API Example api示例完整的事件控制器,它能实际工作的API,可以返回事件和创建、编辑和删除资源。 实体类
export class Event {
id: number;
name: string;
description: string;
when: Date;
address: string;
} 控制器
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
} from '@nestjs/common';
import { CreateEventDto } from './create-event.dto';
import { Event } from './event.entity';
import { UpdateEventDto } from './update-event.dto';
@Controller('/events')
export class EventsController {
private events: Event[] = [];
@Get()
findAll() {
return this.events;
}
@Get(':id')
findOne(@Param('id') id) {
const event = this.events.find((event) => event.id === parseInt(id));
return event;
}
@Post()
create(@Body() input: CreateEventDto) {
const event = {
...input,
when: new Date(input.when),
id: this.events.length + 1,
};
this.events.push(event);
return event;
}
@Patch(':id')
update(@Param('id') id, @Body() input: UpdateEventDto) {
const index = this.events.findIndex((event) => event.id === parseInt(id));
this.events[index] = {
...this.events[index],
...input,
when: input.when ? new Date(input.when) : this.events[index].when,
};
return this.events[index];
}
@Delete(':id')
@HttpCode(204)
remove(@Param('id') id) {
this.events = this.events.filter((event) => event.id !== parseInt(id));
}
} |
05 - Data Validation 数据验证001 Data Validation - Section Introduction 简介Nest中的验证器管道是一组预定义的验证器。 002 Introduction to Pipes 管道管道对数据进行验证,转换,等等。 ParseIntPipe 将数据格式化为整数ParseIntPipe 将请求传入的参数字符串 id 格式化为整数类型 import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
ParseIntPipe,
} from '@nestjs/common';
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id) {
return await this.repository.findOne({
where: {
id,
},
});
} 使用 ParseIntPipe 管道后,可以将id安全的声明为整数 @Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return await this.repository.findOne({
where: {
id,
},
});
} 003 Input Validation 验证输入安装验证器和转换器 npm i -S class-validator class-transformer ###创建控制器单独使用 @Body(ValidationPipe) 验证请求体 import {
ValidationPipe,
} from '@nestjs/common';
@Post()
async create(@Body(ValidationPipe) input: CreateEventDto) {
return await this.repository.save({
...input,
when: new Date(input.when),
});
} CreateEventDto DTO中对每个请求参数定义验证规则
import { IsDateString, IsString, Length } from 'class-validator';
export class CreateEventDto {
@IsString()
@Length(5, 255, { message: 'The name length is wrong' })
name: string;
@Length(5, 255)
description: string;
@IsDateString()
when: string;
@Length(5, 255)
address: string;
} app 全局启用验证器这将对所有控制器进行验证,如果DTO定义了验证装饰器
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Remove line below to enable local ValidationPipe settings
// 全局启用验证器
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap(); create 不再需要 ValidationPipe async create(@Body() input: CreateEventDto) { 004 Validation Groups and Options 验证分组和选项验证分组 类似标签 验证分组需要禁用全局验证管道删除 create-event.dto 为地址参数添加验证分组名 create,update
import { IsDateString, IsString, Length } from 'class-validator';
export class CreateEventDto {
@IsString()
@Length(5, 255, { message: 'The name length is wrong' })
name: string;
@Length(5, 255)
description: string;
@IsDateString()
when: string;
@Length(5, 255, { groups: ['create'] })
@Length(10, 20, { groups: ['update'] })
address: string;
} 控制器中 创建路由使用 带 create 标签的规则验证,更新使用带 update 标签的规则验证
创建路由使用 带 create 标签的规则验证 @Post()
async create(
@Body(new ValidationPipe({ groups: ['create'] })) input: CreateEventDto,
) {
return await this.repository.save({
...input,
when: new Date(input.when),
});
} 更新使用带 update 标签的规则验证 @Patch(':id')
async update(
@Param('id') id,
@Body(new ValidationPipe({ groups: ['create'] })) input: UpdateEventDto,
) {
const event = await this.repository.findOne({
where: {
id,
},
});
return await this.repository.save({
...event,
...input,
when: input.when ? new Date(input.when) : event.when,
});
} 使用管道装饰器在指定方法上或指定类上应用验证管道
import {
UsePipes,
} from '@nestjs/common';
@UsePipes(new ValidationPipe({ groups: ['create'] }))
@Post()
async create(@Body() input: CreateEventDto) {
return await this.repository.save({
...input,
when: new Date(input.when),
});
} |
06 - Modules, Providers, Dependency Injection 模块,提供者,依赖注入001 Modules, Providers, Dependency Injection - Section Introduction在抽象支付服务中,需要处理支付,然后发送支付结果邮件。 应将具体的支付服务例如微信支付,支付宝支付作为独立的服务注入到支付服务中。 将发送邮件服务也作为作为独立的服务注入到支付服务中。 这是代码模块化,易于测试。 且具体支付服务可以mock。只测试核心支付服务。 模块每个模块应当只处理特定的任务。 002 Introduction to Modules, Providers and Dependency Injection模块的提供者将注入到其它类中使用。 003 Creating a Custom Module 创建自定义模块创建事件模块nest generate module events
import { Module } from '@nestjs/common';
@Module({})
export class EventsModule {} 这回自动将 EventsModule 导入到app中 import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Event } from './event.entity';
import { EventsController } from './events.controller';
import { EventsModule } from './events/events.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: '127.0.0.1', //在外部运行该程序时使用'127.0.0.1',在容器内运行程序时为 mysql 容器名
port: 3307,
username: 'root',
password: 'example',
database: 'nest-events',
entities: [Event],
synchronize: true,
}),
TypeOrmModule.forFeature([Event]),
EventsModule,
],
controllers: [AppController, EventsController],
providers: [AppService],
})
export class AppModule {} 整理文件,将属于 EventsModule 的文件放入
|
07 - Configuration, Logging, and Errors 配置,日志001 Application Config and Environments 应用程序配置和环境变量不同的环境将使用不同的数据库等等。 安装配置模块npm i -S @nestjs/config app 模块中注册配置模块仅在当前模块下有效
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot(),
],
})
export class AppModule {} import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Event } from './events/event.entity';
import { EventsModule } from './events/events.module';
import { AppChineseService } from './app.chinese.service';
import { AppDummy } from './app.dummy';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forRoot({
type: 'mysql',
host: '127.0.0.1',
port: 3307,
username: 'root',
password: 'example',
database: 'nest-events',
entities: [Event],
synchronize: true,
}),
EventsModule,
],
controllers: [AppController],
providers: [
{
provide: AppService,
//useClass: AppService,
useClass: AppChineseService,
},
{
provide: 'APP_NAME',
useValue: 'Nest Events Backend!',
},
{
provide: 'MESSAGE',
inject: [AppDummy], //将标准提供者AppDummy注入工厂函数
useFactory: (app) => `${app.dummy()} Factory!`, //注入的提供者将成为工厂函数的参数 app = AppDummy
},
AppDummy, //成为标准提供者
],
})
export class AppModule {} 每个环境的shell都有自己环境变量,变量可以在node中通过 process.env 读取。 例如: 通过
|
08 - Intermediate Database Concepts 中间数据库概念001 Understanding Relations 理解实体之间的数据库关系002 One To Many Relation 一对多关系一个出席者对应一个会议事件 事件的出席者实体出席者的 event 关联到事件的 event.attendees
可以手动指定列名 默认将指向 event 表的 id 主键。
import {
Column,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Event } from './event.entity';
@Entity()
export class Attendee {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToOne(() => Event, (event) => event.attendees, {
nullable: false,
})
@JoinColumn()
//@JoinColumn({ name: 'event_id', referencedColumnName: 'id' })
event: Event;
} 事件实体中添加出席者事件的 attendees 关联到 出席者的 attendee.event import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Attendee } from './attendee.entity';
@Entity()
export class Event {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
description: string;
@Column()
when: Date;
@Column()
address: string;
@OneToMany(() => Attendee, (attendee) => attendee.event)
attendees: Attendee[];
} 配置出席者实体
import { registerAs } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { Attendee } from 'src/events/attendee.entity';
import { Event } from './../events/event.entity';
export default registerAs(
'orm.config',
(): TypeOrmModuleOptions => ({
type: 'mysql',
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: [Event, Attendee],
synchronize: true,
}),
);
import { registerAs } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { Event } from './../events/event.entity';
import { Attendee } from 'src/events/attendee.entity';
export default registerAs(
'orm.config',
(): TypeOrmModuleOptions => ({
type: 'mysql',
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: [Event, Attendee],
synchronize: false,
}),
); 003 Loading Related Entities 读取相关实体插入测试数据INSERT INTO
`event` (`id`, `description`, `when`, `address`, `name`)
VALUES
(
1,
'Let\'s meet together.',
'2021-02-15 21:00:00',
'Office St 120',
'Team Meetup'
),
(
2,
'Let\'s learn something.',
'2021-02-17 21:00:00',
'Workshop St 80',
'Workshop'
),
(
3,
'Let\'s meet with big bosses',
'2021-02-17 21:00:00',
'Boss St 100',
'Strategy Meeting'
),
(
4,
'Let\'s try to sell stuff',
'2021-02-11 21:00:00',
'Money St 34',
'Sales Pitch'
),
(
5,
'People meet to talk about business ideas',
'2021-02-12 21:00:00',
'Invention St 123',
'Founders Meeting'
);
INSERT INTO
`attendee` (`id`, `name`, `eventId`)
VALUES
(1, 'Piotr', 1),
(2, 'John', 1),
(3, 'Terry', 1),
(4, 'Bob', 2),
(5, 'Joe', 2),
(6, 'Donald', 2),
(7, 'Harry', 4); 同步加载当前实体的关联实体 eager: true在加载当前实体后,将硬加载其关联的实体,性能消耗更大,不推荐。
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Attendee } from './attendee.entity';
@Entity()
export class Event {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
description: string;
@Column()
when: Date;
@Column()
address: string;
@OneToMany(() => Attendee, (attendee) => attendee.event, { eager: true })
attendees: Attendee[];
}
@Get('practice2')
async practice2() {
return await this.repository.findOne({
where: {
id: 1,
},
});
} 这将把事件的关联实体出席者attendees的全部数据返回 在当前实体配置了全局同步加载关联实体后,在具体的方法中取消同步加载关联实体
|
09 - Authentication, JWT, Authorization 身份验证、JWT、授权001 Introduction to Authentication002 Local Passport Strategy 本地密码策略使用用户名和密码进行验证 安装密码库npm i -S @nestjs/passport passport passport-local
npm i -D @types/passport-local 创建auth验证模块
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
@Module({
imports: [],
providers: [],
})
export class AuthModule {} app中添加验证模块
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppDummy } from './app.dummy';
import { AppChineseService } from './app.chinese.service';
import { AppService } from './app.service';
import ormConfig from './config/orm.config';
import ormConfigProd from './config/orm.config.prod';
import { EventsModule } from './events/events.module';
import { SchoolModule } from './school/school.module';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [
// 使用 ormConfig 作为通用环境配置
ConfigModule.forRoot({
isGlobal: true,
load: [ormConfig], //通过配置服务读取配置
expandVariables: true, //启用可扩展环境变量 以支持字符串变量 ${APP_URL}
}),
// 异步加载工厂函数配置 分别用以数据库配置
TypeOrmModule.forRootAsync({
useFactory:
process.env.NODE_ENV !== 'production' ? ormConfig : ormConfigProd,
}),
AuthModule,
EventsModule,
SchoolModule,
],
controllers: [AppController],
providers: [
{
provide: AppService,
useClass: AppChineseService,
},
{
provide: 'APP_NAME',
useValue: 'Nest Events Backend!',
},
{
provide: 'MESSAGE',
inject: [AppDummy],
useFactory: (app) => `${app.dummy()} Factory!`,
},
AppDummy,
],
})
export class AppModule {} 创建本地验证策略,使其可注入
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { Strategy } from 'passport-local';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
private readonly logger = new Logger(LocalStrategy.name);
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {
super();
}
public async validate(username: string, password: string): Promise<any> {
const user = await this.userRepository.findOne({
where: { username },
});
if (!user) {
this.logger.debug(`User ${username} not found!`);
throw new UnauthorizedException();
}
if (password !== user.password) {
this.logger.debug(`Invalid credentials for user ${username}`);
throw new UnauthorizedException();
}
return user;
}
} auth验证模块 导入用户实体和本地策略
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LocalStrategy } from './local.strategy';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [LocalStrategy],
})
export class AuthModule {} 003 Logging In - Passport Strategy with a Nest Guard 登录-带Nest Guard的Passport策略验证控制器
import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller('auth')
export class AuthController {
} 注册验证控制器
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { LocalStrategy } from './local.strategy';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [LocalStrategy],
controllers: [AuthController],
})
export class AuthModule {} 控制器中添加登录路由,使用验证守卫和默认的本地策略
import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller('auth')
export class AuthController {
@Post('login')
@UseGuards(AuthGuard('local')) //本地验证策略的默认名称为 local
async login(@Request() request) {
return {
userId: request.user.id,
token: 'the token will go here',
};
}
} AuthGuard('local') 默认从请求体获取 username 和 password 来验证 004 JWT - JSON Web Tokens Introduction JWT-JSON Web令牌简介令牌不需要数据库查询,且负载应尽量小。 005 JWT - Generating Token JWT-生成令牌jwt库npm i -S @nestjs/jwt passport-jwt
npm i -D @types/passport-jwt auth模块中注册jwt模块
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { LocalStrategy } from './local.strategy';
import { User } from './user.entity';
@Module({
imports: [
TypeOrmModule.forFeature([User]),
JwtModule.registerAsync({
useFactory: () => ({
secret: process.env.AUTH_SECRET,
signOptions: {
expiresIn: '60m',
},
}),
}),
],
providers: [LocalStrategy],
controllers: [AuthController],
})
export class AuthModule {} env 环境变量添加jwt密钥
DB_HOST=localhost
DB_PORT=3307
DB_USER=root
DB_PASSWORD=example
DB_NAME=nest-events
APP_URL=mywebsite.com
SUPPORT_EMAIL=support@${APP_URL}
AUTH_SECRET=secret123 验证服务
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { User } from './user.entity';
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService) {}
public getTokenForUser(user: User): string {
return this.jwtService.sign({
username: user.username,
sub: user.id,
});
}
} 验证模块中注册验证服务为提供者环境变量在创建验证模块时不可用,需要使用工厂函数和异步配置jwt模块
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { User } from './user.entity';
@Module({
imports: [
TypeOrmModule.forFeature([User]),
JwtModule.registerAsync({
useFactory: () => ({
secret: process.env.AUTH_SECRET,
signOptions: {
expiresIn: '60m',
},
}),
}),
],
providers: [LocalStrategy, AuthService],
controllers: [AuthController],
})
export class AuthModule {} 控制器中生成jwt
import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@UseGuards(AuthGuard('local'))
async login(@Request() request) {
console.log(process.env.AUTH_SECRET);
return {
userId: request.user.id,
token: this.authService.getTokenForUser(request.user),
};
}
} 006 JWT - Strategy & Guard - Authenticating with JWT Token JWT-策略与保护-使用JWT令牌进行身份验证创建策略使用jwt令牌进行验证
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false, //令牌过期后是否忽略,false-过期则需要重新登陆
secretOrKey: process.env.AUTH_SECRET,
});
}
async validate(payload: any) {
return await this.userRepository.findOne({ where: { id: payload.sub } });//返回的{user:...}数据将被填充到request中供后续路由使用
}
} 控制器添加获取用户资料getProfile路由,使用jwt令牌策略验证
import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@UseGuards(AuthGuard('local'))
async login(@Request() request) {
console.log(process.env.AUTH_SECRET);
return {
userId: request.user.id,
token: this.authService.getTokenForUser(request.user),
};
}
@Get('profile')
@UseGuards(AuthGuard('jwt'))
async getProfile(@Request() request) {
return request.user;
}
} 验证模块注册jwt策略为提供者
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { LocalStrategy } from './local.strategy';
import { User } from './user.entity';
@Module({
imports: [
TypeOrmModule.forFeature([User]),
JwtModule.registerAsync({
useFactory: () => ({
secret: process.env.AUTH_SECRET,
signOptions: {
expiresIn: '60m',
},
}),
}),
],
providers: [LocalStrategy, JwtStrategy, AuthService],
controllers: [AuthController],
})
export class AuthModule {} 请求 profile添加请求头 007 Hashing Passwords with Bcrypt 使用Bcrypt哈希密码Bcryptnpm i -S bcrypt
npm i -D @types/bcrypt 验证服务添加加密功能
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { User } from './user.entity';
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService) {}
public getTokenForUser(user: User): string {
return this.jwtService.sign({
username: user.username,
sub: user.id,
});
}
public async hashPassword(password: string): Promise<string> {
return await bcrypt.hash(password, 10);
}
} 本地密码策略更新使用加密比较密码
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import * as bcrypt from 'bcrypt';
import { Strategy } from 'passport-local';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
private readonly logger = new Logger(LocalStrategy.name);
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {
super();
}
public async validate(username: string, password: string): Promise<any> {
const user = await this.userRepository.findOne({
where: { username },
});
if (!user) {
this.logger.debug(`User ${username} not found!`);
throw new UnauthorizedException();
}
if (!(await bcrypt.compare(password, user.password))) {
this.logger.debug(`Invalid credentials for user ${username}`);
throw new UnauthorizedException();
}
return user;
}
} 008 Custom CurrentUser Decorator 自定义CurrentUser装饰器使用自定义参数对用户进行验证 创建用户自定义参数装饰器 CurrentUser
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user ?? null;
},
); 使用CurrentUser 装饰器
import { Controller, Get, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { CurrentUser } from './current-user.decorator';
import { User } from './user.entity';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@UseGuards(AuthGuard('local'))
async login(@CurrentUser() user: User) {
return {
userId: user.id,
token: this.authService.getTokenForUser(user),
};
}
@Get('profile')
@UseGuards(AuthGuard('jwt'))
async getProfile(@CurrentUser() user: User) {
return user;
}
} 009 User Registration 用户注册更新用户实体
import {
Column,
Entity,
JoinColumn,
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Profile } from './profile.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
username: string;
@Column()
password: string;
@Column({ unique: true })
email: string;
@Column()
firstName: string;
@Column()
lastName: string;
@OneToOne(() => Profile)
@JoinColumn()
profile: Profile;
} 添加用户DTO
import { IsEmail, Length } from 'class-validator';
export class CreateUserDto {
@Length(5)
username: string;
@Length(8)
password: string;
@Length(8)
retypedPassword: string;
@Length(2)
firstName: string;
@Length(2)
lastName: string;
@IsEmail()
email: string;
} 添加用户控制器
import { BadRequestException, Body, Controller, Post } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AuthService } from './auth.service';
import { CreateUserDto } from './input/create.user.dto';
import { User } from './user.entity';
@Controller('users')
export class UsersController {
constructor(
private readonly authService: AuthService,
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
@Post()
async create(@Body() createUserDto: CreateUserDto) {
const user = new User();
if (createUserDto.password !== createUserDto.retypedPassword) {
throw new BadRequestException(['Passwords are not identical']);
}
const existingUser = await this.userRepository.findOne({
where: [
{ username: createUserDto.username },
{ email: createUserDto.email },
],
});
if (existingUser) {
throw new BadRequestException(['username or email is already taken']);
}
user.username = createUserDto.username;
user.password = await this.authService.hashPassword(createUserDto.password);
user.email = createUserDto.email;
user.firstName = createUserDto.firstName;
user.lastName = createUserDto.lastName;
return {
...(await this.userRepository.save(user)),
token: this.authService.getTokenForUser(user),
};
}
} 验证模块注册用户控制器
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { LocalStrategy } from './local.strategy';
import { User } from './user.entity';
import { UsersController } from './users.controller';
@Module({
imports: [
TypeOrmModule.forFeature([User]),
JwtModule.registerAsync({
useFactory: () => ({
secret: process.env.AUTH_SECRET,
signOptions: {
expiresIn: '60m',
},
}),
}),
],
providers: [LocalStrategy, JwtStrategy, AuthService],
controllers: [AuthController, UsersController],
})
export class AuthModule {} 请求注册路由post localhost:3000/auth/create 010 Only Authenticated Users Can Create Events 只有经过身份验证的用户才能创建事件在事件和用户之间建立联系 organizer一个组织者对应多个事件
import { User } from 'src/auth/user.entity';
import {
Column,
Entity,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Attendee } from './attendee.entity';
@Entity()
export class Event {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
description: string;
@Column()
when: Date;
@Column()
address: string;
@OneToMany(() => Attendee, (attendee) => attendee.event, {
cascade: true,
})
attendees: Attendee[];
@ManyToOne(() => User, (user) => user.organized)
organizer: User;
@Column({ nullable: true })
organizerId: number;
attendeeCount?: number;
attendeeRejected?: number;
attendeeMaybe?: number;
attendeeAccepted?: number;
}
import {
Column,
Entity,
JoinColumn,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Event } from './../events/event.entity';
import { Profile } from './profile.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
username: string;
@Column()
password: string;
@Column({ unique: true })
email: string;
@Column()
firstName: string;
@Column()
lastName: string;
@OneToOne(() => Profile)
@JoinColumn()
profile: Profile;
@OneToMany(() => Event, (event) => event.organizer)
organized: Event[];
} 事件服务添加创建事件 createEvent
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/auth/user.entity';
import { paginate, PaginateOptions } from 'src/pagination/paginator';
import { DeleteResult, Repository } from 'typeorm';
import { AttendeeAnswerEnum } from './attendee.entity';
import { Event } from './event.entity';
import { CreateEventDto } from './input/create-event.dto';
import { ListEvents, WhenEventFilter } from './input/list.events';
@Injectable()
export class EventsService {
private readonly logger = new Logger(EventsService.name);
constructor(
@InjectRepository(Event)
private readonly eventsRepository: Repository<Event>,
) {}
private getEventsBaseQuery() {
return this.eventsRepository
.createQueryBuilder('e')
.orderBy('e.id', 'DESC');
}
public getEventsWithAttendeeCountQuery() {
return this.getEventsBaseQuery()
.loadRelationCountAndMap('e.attendeeCount', 'e.attendees')
.loadRelationCountAndMap(
'e.attendeeAccepted',
'e.attendees',
'attendee',
(qb) =>
qb.where('attendee.answer = :answer', {
answer: AttendeeAnswerEnum.Accepted,
}),
)
.loadRelationCountAndMap(
'e.attendeeMaybe',
'e.attendees',
'attendee',
(qb) =>
qb.where('attendee.answer = :answer', {
answer: AttendeeAnswerEnum.Maybe,
}),
)
.loadRelationCountAndMap(
'e.attendeeRejected',
'e.attendees',
'attendee',
(qb) =>
qb.where('attendee.answer = :answer', {
answer: AttendeeAnswerEnum.Rejected,
}),
);
}
private async getEventsWithAttendeeCountFiltered(filter?: ListEvents) {
let query = this.getEventsWithAttendeeCountQuery();
if (!filter) {
return query;
}
if (filter.when) {
if (filter.when == WhenEventFilter.Today) {
query = query.andWhere(
`e.when >= CURDATE() AND e.when <= CURDATE() + INTERVAL 1 DAY`,
);
}
if (filter.when == WhenEventFilter.Tommorow) {
query = query.andWhere(
`e.when >= CURDATE() + INTERVAL 1 DAY AND e.when <= CURDATE() + INTERVAL 2 DAY`,
);
}
if (filter.when == WhenEventFilter.ThisWeek) {
query = query.andWhere('YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1)');
}
if (filter.when == WhenEventFilter.NextWeek) {
query = query.andWhere(
'YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1) + 1',
);
}
}
return query;
}
public async getEventsWithAttendeeCountFilteredPaginated(
filter: ListEvents,
paginateOptions: PaginateOptions,
) {
return await paginate(
await this.getEventsWithAttendeeCountFiltered(filter),
paginateOptions,
);
}
public async getEvent(id: number): Promise<Event | undefined> {
const query = this.getEventsWithAttendeeCountQuery().andWhere(
'e.id = :id',
{ id },
);
this.logger.debug(query.getSql());
return await query.getOne();
}
public async createEvent(input: CreateEventDto, user: User): Promise<Event> {
console.log(user);
return await this.eventsRepository.save({
...input,
organizer: user,
when: new Date(input.when),
});
}
public async deleteEvent(id: number): Promise<DeleteResult> {
return await this.eventsRepository
.createQueryBuilder('e')
.delete()
.where('id = :id', { id })
.execute();
}
} 事件控制器创建事件
import { User } from 'src/auth/user.entity';
// You can also use the @UsePipes decorator to enable pipes.
// It can be done per method, or for every method when you
// add it at the controller level.
@Post()
async create(@Body() input: CreateEventDto, @CurrentUser() user: User) {
return await this.eventsService.createEvent(input, user);
} 优化验证模块 创建独立的验证守卫
import { AuthGuard } from '@nestjs/passport';
export class AuthGuardJwt extends AuthGuard('jwt') {}
import { AuthGuard } from '@nestjs/passport';
export class AuthGuardLocal extends AuthGuard('local') {} 使用新的验证守卫
import { Controller, Get, Post, UseGuards } from '@nestjs/common';
import { AuthGuardJwt } from './auth-guard.jwt';
import { AuthGuardLocal } from './auth-guard.local';
import { AuthService } from './auth.service';
import { CurrentUser } from './current-user.decorator';
import { User } from './user.entity';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@UseGuards(AuthGuardLocal)
async login(@CurrentUser() user: User) {
return {
userId: user.id,
token: this.authService.getTokenForUser(user),
};
}
@Get('profile')
@UseGuards(AuthGuardJwt)
async getProfile(@CurrentUser() user: User) {
return user;
}
} 事件创建使用jwt验证守卫
import { AuthGuardJwt } from 'src/auth/auth-guard.jwt';
import { User } from 'src/auth/user.entity';
// You can also use the @UsePipes decorator to enable pipes.
// It can be done per method, or for every method when you
// add it at the controller level.
@Post()
@UseGuards(AuthGuardJwt)
async create(@Body() input: CreateEventDto, @CurrentUser() user: User) {
return await this.eventsService.createEvent(input, user);
} 011 Only The Owners Can Edit or Delete Events 只有所有者才能编辑或删除事件更新方法提取到服务中
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/auth/user.entity';
import { paginate, PaginateOptions } from 'src/pagination/paginator';
import { DeleteResult, Repository } from 'typeorm';
import { AttendeeAnswerEnum } from './attendee.entity';
import { Event } from './event.entity';
import { CreateEventDto } from './input/create-event.dto';
import { ListEvents, WhenEventFilter } from './input/list.events';
import { UpdateEventDto } from './input/update-event.dto';
@Injectable()
export class EventsService {
private readonly logger = new Logger(EventsService.name);
constructor(
@InjectRepository(Event)
private readonly eventsRepository: Repository<Event>,
) {}
private getEventsBaseQuery() {
return this.eventsRepository
.createQueryBuilder('e')
.orderBy('e.id', 'DESC');
}
public getEventsWithAttendeeCountQuery() {
return this.getEventsBaseQuery()
.loadRelationCountAndMap('e.attendeeCount', 'e.attendees')
.loadRelationCountAndMap(
'e.attendeeAccepted',
'e.attendees',
'attendee',
(qb) =>
qb.where('attendee.answer = :answer', {
answer: AttendeeAnswerEnum.Accepted,
}),
)
.loadRelationCountAndMap(
'e.attendeeMaybe',
'e.attendees',
'attendee',
(qb) =>
qb.where('attendee.answer = :answer', {
answer: AttendeeAnswerEnum.Maybe,
}),
)
.loadRelationCountAndMap(
'e.attendeeRejected',
'e.attendees',
'attendee',
(qb) =>
qb.where('attendee.answer = :answer', {
answer: AttendeeAnswerEnum.Rejected,
}),
);
}
private async getEventsWithAttendeeCountFiltered(filter?: ListEvents) {
let query = this.getEventsWithAttendeeCountQuery();
if (!filter) {
return query;
}
if (filter.when) {
if (filter.when == WhenEventFilter.Today) {
query = query.andWhere(
`e.when >= CURDATE() AND e.when <= CURDATE() + INTERVAL 1 DAY`,
);
}
if (filter.when == WhenEventFilter.Tommorow) {
query = query.andWhere(
`e.when >= CURDATE() + INTERVAL 1 DAY AND e.when <= CURDATE() + INTERVAL 2 DAY`,
);
}
if (filter.when == WhenEventFilter.ThisWeek) {
query = query.andWhere('YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1)');
}
if (filter.when == WhenEventFilter.NextWeek) {
query = query.andWhere(
'YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1) + 1',
);
}
}
return query;
}
public async getEventsWithAttendeeCountFilteredPaginated(
filter: ListEvents,
paginateOptions: PaginateOptions,
) {
return await paginate(
await this.getEventsWithAttendeeCountFiltered(filter),
paginateOptions,
);
}
public async getEvent(id: number): Promise<Event | undefined> {
const query = this.getEventsWithAttendeeCountQuery().andWhere(
'e.id = :id',
{ id },
);
this.logger.debug(query.getSql());
return await query.getOne();
}
public async createEvent(input: CreateEventDto, user: User): Promise<Event> {
console.log(user);
return await this.eventsRepository.save({
...input,
organizer: user,
when: new Date(input.when),
});
}
public async updateEvent(
event: Event,
input: UpdateEventDto,
): Promise<Event> {
return await this.eventsRepository.save({
...event,
...input,
when: input.when ? new Date(input.when) : event.when,
});
}
public async deleteEvent(id: number): Promise<DeleteResult> {
return await this.eventsRepository
.createQueryBuilder('e')
.delete()
.where('id = :id', { id })
.execute();
}
} 控制器不再需要注入存储库 repository,为更新和删除添加验证,必须是组织者可以操作事件
import {
Body,
Controller,
Delete,
ForbiddenException,
Get,
HttpCode,
Logger,
NotFoundException,
Param,
ParseIntPipe,
Patch,
Post,
Query,
UseGuards,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { AuthGuardJwt } from 'src/auth/auth-guard.jwt';
import { CurrentUser } from 'src/auth/current-user.decorator';
import { User } from 'src/auth/user.entity';
import { EventsService } from './events.service';
import { CreateEventDto } from './input/create-event.dto';
import { ListEvents } from './input/list.events';
import { UpdateEventDto } from './input/update-event.dto';
@Controller('/events')
export class EventsController {
private readonly logger = new Logger(EventsController.name);
constructor(private readonly eventsService: EventsService) {}
@Get()
@UsePipes(new ValidationPipe({ transform: true }))
async findAll(@Query() filter: ListEvents) {
const events =
await this.eventsService.getEventsWithAttendeeCountFilteredPaginated(
filter,
{
total: true,
currentPage: filter.page,
limit: 2,
},
);
return events;
}
@Get('/practice')
async practice() {
// return await this.repository.find({
// select: ['id', 'when'],
// where: [
// {
// id: MoreThan(3),
// when: MoreThan(new Date('2021-02-12T13:00:00')),
// },
// {
// description: Like('%meet%'),
// },
// ],
// take: 2,
// order: {
// id: 'DESC',
// },
// });
}
@Get('practice2')
async practice2() {
// const event = await this.repository.findOne({
// where: {
// id: 1,
// },
// //loadEagerRelations: false,
// relations: ['attendees'],
// });
// const attendee = new Attendee();
// attendee.name = 'Potter3';
// // attendee.event = event; // 级联将导致循环引用错误
// event.attendees.push(attendee);
// await this.repository.save(event);
// return event;
// return await this.repository
// .createQueryBuilder('e')
// .select(['e.id', 'e.name'])
// .orderBy('e.id', 'ASC')
// .take(3)
// .getMany();
}
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
// const event = await this.repository.findOne({
// where: {
// id,
// },
// });
const event = await this.eventsService.getEvent(id);
if (!event) {
throw new NotFoundException();
}
return event;
}
// You can also use the @UsePipes decorator to enable pipes.
// It can be done per method, or for every method when you
// add it at the controller level.
@Post()
@UseGuards(AuthGuardJwt)
async create(@Body() input: CreateEventDto, @CurrentUser() user: User) {
return await this.eventsService.createEvent(input, user);
}
// Create new ValidationPipe to specify validation group inside @Body
// new ValidationPipe({ groups: ['update'] })
@Patch(':id')
@UseGuards(AuthGuardJwt)
async update(
@Param('id') id,
@Body() input: UpdateEventDto,
@CurrentUser() user: User,
) {
const event = await this.eventsService.getEvent(id);
if (!event) {
throw new NotFoundException();
}
if (event.organizerId !== user.id) {
throw new ForbiddenException(
null,
`You are not authorized to change this event`,
);
}
return await this.eventsService.updateEvent(event, input);
}
@Delete(':id')
@UseGuards(AuthGuardJwt)
@HttpCode(204)
async remove(@Param('id') id, @CurrentUser() user: User) {
const event = await this.eventsService.getEvent(id);
if (!event) {
throw new NotFoundException();
}
if (event.organizerId !== user.id) {
throw new ForbiddenException(
null,
`You are not authorized to remove this event`,
);
}
await this.eventsService.deleteEvent(id);
}
} |
10 - Data Serialization 数据序列化001 Interceptors and Serialization 拦截器和序列化拦截器可以转换数据,转换异常,增加缓存功能。可以修改控制器返回的数据。 序列化转换实体的json数据,只响应需要的数据给用户。 002 Serializing Data 序列化序列化 有两种策略,排除所有实体参数或公开所有实体参数 事件控制器添加序列化装饰器 @SerializeOptions({ strategy: 'excludeAll' })
import {
Body,
ClassSerializerInterceptor,
Controller,
Delete,
ForbiddenException,
Get,
HttpCode,
Logger,
NotFoundException,
Param,
ParseIntPipe,
Patch,
Post,
Query,
SerializeOptions,
UseGuards,
UseInterceptors,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { AuthGuardJwt } from 'src/auth/auth-guard.jwt';
import { CurrentUser } from 'src/auth/current-user.decorator';
import { User } from 'src/auth/user.entity';
import { EventsService } from './events.service';
import { CreateEventDto } from './input/create-event.dto';
import { ListEvents } from './input/list.events';
import { UpdateEventDto } from './input/update-event.dto';
@Controller('/events')
@SerializeOptions({ strategy: 'excludeAll' })
export class EventsController { 获取路由添加序列化拦截装饰器 @UseInterceptors(ClassSerializerInterceptor) @Get(':id')
@UseInterceptors(ClassSerializerInterceptor)
async findOne(@Param('id', ParseIntPipe) id: number) {
// const event = await this.repository.findOne({
// where: {
// id,
// },
// });
const event = await this.eventsService.getEvent(id);
if (!event) {
throw new NotFoundException();
}
return event;
} 实体中定义序列化时应该公开那些参数 @expose()公开 的参数才会响应给用户
import { Expose } from 'class-transformer';
import { User } from 'src/auth/user.entity';
import {
Column,
Entity,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Attendee } from './attendee.entity';
@Entity()
export class Event {
@PrimaryGeneratedColumn()
@Expose()
id: number;
@Column()
@Expose()
name: string;
@Column()
@Expose()
description: string;
@Column()
@Expose()
when: Date;
@Column()
@Expose()
address: string;
@OneToMany(() => Attendee, (attendee) => attendee.event, {
cascade: true,
})
@Expose()
attendees: Attendee[];
@ManyToOne(() => User, (user) => user.organized)
@Expose()
organizer: User;
@Column({ nullable: true })
organizerId: number;
@Expose()
attendeeCount?: number;
@Expose()
attendeeRejected?: number;
@Expose()
attendeeMaybe?: number;
@Expose()
attendeeAccepted?: number;
}
import { Expose } from 'class-transformer';
import {
Column,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Event } from './event.entity';
export enum AttendeeAnswerEnum {
Accepted = 1,
Maybe,
Rejected,
}
@Entity()
export class Attendee {
@PrimaryGeneratedColumn()
@Expose()
id: number;
@Column()
@Expose()
name: string;
@ManyToOne(() => Event, (event) => event.attendees, {
nullable: true,
})
@JoinColumn()
event: Event;
@Column('enum', {
enum: AttendeeAnswerEnum,
default: AttendeeAnswerEnum.Accepted,
})
@Expose()
answer: AttendeeAnswerEnum;
}
import { Expose } from 'class-transformer';
import {
Column,
Entity,
JoinColumn,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Event } from './../events/event.entity';
import { Profile } from './profile.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
@Expose()
id: number;
@Column({ unique: true })
@Expose()
username: string;
@Column()
password: string;
@Column({ unique: true })
@Expose()
email: string;
@Column()
@Expose()
firstName: string;
@Column()
@Expose()
lastName: string;
@OneToOne(() => Profile)
@JoinColumn()
@Expose()
profile: Profile;
@OneToMany(() => Event, (event) => event.organizer)
@Expose()
organized: Event[];
} 验证控制器添加序列化和拦截器
import {
ClassSerializerInterceptor,
Controller,
Get,
Post,
SerializeOptions,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { AuthGuardJwt } from './auth-guard.jwt';
import { AuthGuardLocal } from './auth-guard.local';
import { AuthService } from './auth.service';
import { CurrentUser } from './current-user.decorator';
import { User } from './user.entity';
@Controller('auth')
@SerializeOptions({ strategy: 'excludeAll' })
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@UseGuards(AuthGuardLocal)
async login(@CurrentUser() user: User) {
return {
userId: user.id,
token: this.authService.getTokenForUser(user),
};
}
@Get('profile')
@UseGuards(AuthGuardJwt)
@UseInterceptors(ClassSerializerInterceptor)
async getProfile(@CurrentUser() user: User) {
return user;
}
} 事件控制器补全拦截器
import {
Body,
ClassSerializerInterceptor,
Controller,
Delete,
ForbiddenException,
Get,
HttpCode,
Logger,
NotFoundException,
Param,
ParseIntPipe,
Patch,
Post,
Query,
SerializeOptions,
UseGuards,
UseInterceptors,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { AuthGuardJwt } from 'src/auth/auth-guard.jwt';
import { CurrentUser } from 'src/auth/current-user.decorator';
import { User } from 'src/auth/user.entity';
import { EventsService } from './events.service';
import { CreateEventDto } from './input/create-event.dto';
import { ListEvents } from './input/list.events';
import { UpdateEventDto } from './input/update-event.dto';
@Controller('/events')
@SerializeOptions({ strategy: 'excludeAll' })
export class EventsController {
private readonly logger = new Logger(EventsController.name);
constructor(private readonly eventsService: EventsService) {}
@Get()
@UsePipes(new ValidationPipe({ transform: true }))
@UseInterceptors(ClassSerializerInterceptor)
async findAll(@Query() filter: ListEvents) {
const events =
await this.eventsService.getEventsWithAttendeeCountFilteredPaginated(
filter,
{
total: true,
currentPage: filter.page,
limit: 2,
},
);
return events;
}
@Get('/practice')
async practice() {
// return await this.repository.find({
// select: ['id', 'when'],
// where: [
// {
// id: MoreThan(3),
// when: MoreThan(new Date('2021-02-12T13:00:00')),
// },
// {
// description: Like('%meet%'),
// },
// ],
// take: 2,
// order: {
// id: 'DESC',
// },
// });
}
@Get('practice2')
async practice2() {
// const event = await this.repository.findOne({
// where: {
// id: 1,
// },
// //loadEagerRelations: false,
// relations: ['attendees'],
// });
// const attendee = new Attendee();
// attendee.name = 'Potter3';
// // attendee.event = event; // 级联将导致循环引用错误
// event.attendees.push(attendee);
// await this.repository.save(event);
// return event;
// return await this.repository
// .createQueryBuilder('e')
// .select(['e.id', 'e.name'])
// .orderBy('e.id', 'ASC')
// .take(3)
// .getMany();
}
@Get(':id')
@UseInterceptors(ClassSerializerInterceptor)
async findOne(@Param('id', ParseIntPipe) id: number) {
// const event = await this.repository.findOne({
// where: {
// id,
// },
// });
const event = await this.eventsService.getEvent(id);
if (!event) {
throw new NotFoundException();
}
return event;
}
// You can also use the @UsePipes decorator to enable pipes.
// It can be done per method, or for every method when you
// add it at the controller level.
@Post()
@UseGuards(AuthGuardJwt)
@UseInterceptors(ClassSerializerInterceptor)
async create(@Body() input: CreateEventDto, @CurrentUser() user: User) {
return await this.eventsService.createEvent(input, user);
}
// Create new ValidationPipe to specify validation group inside @Body
// new ValidationPipe({ groups: ['update'] })
@Patch(':id')
@UseGuards(AuthGuardJwt)
@UseInterceptors(ClassSerializerInterceptor)
async update(
@Param('id') id,
@Body() input: UpdateEventDto,
@CurrentUser() user: User,
) {
const event = await this.eventsService.getEvent(id);
if (!event) {
throw new NotFoundException();
}
if (event.organizerId !== user.id) {
throw new ForbiddenException(
null,
`You are not authorized to change this event`,
);
}
return await this.eventsService.updateEvent(event, input);
}
@Delete(':id')
@UseGuards(AuthGuardJwt)
@HttpCode(204)
async remove(@Param('id') id, @CurrentUser() user: User) {
const event = await this.eventsService.getEvent(id);
if (!event) {
throw new NotFoundException();
}
if (event.organizerId !== user.id) {
throw new ForbiddenException(
null,
`You are not authorized to remove this event`,
);
}
await this.eventsService.deleteEvent(id);
}
} 由于 findAll 返回的对象不是实例化的类:ClassSerializerInterceptor依赖类的元数据来应用序列化规则。如果返回的数据不是通过类构造函数创建的实例,而是简单的对象字面量,那么可能不会应用任何序列化规则。 003 Serializing Nested Objects 序列化嵌套对象序列化的目标必须是实体的类的实例。分页返回类的实例 PaginationResult序列化时,需要通过实体的 @expose() 参数决定是否返回
import { Expose } from 'class-transformer';
import { SelectQueryBuilder } from 'typeorm';
export interface PaginateOptions {
limit: number;
currentPage: number;
total?: boolean;
}
export class PaginationResult<T> {
constructor(partial: Partial<PaginationResult<T>>) {
Object.assign(this, partial);
}
@Expose()
first: number;
@Expose()
last: number;
@Expose()
limit: number;
@Expose()
total?: number;
@Expose()
data: T[];
}
export async function paginate<T>(
qb: SelectQueryBuilder<T>,
options: PaginateOptions = {
limit: 10,
currentPage: 1,
},
): Promise<PaginationResult<T>> {
const offset = (options.currentPage - 1) * options.limit;
const data = await qb.limit(options.limit).offset(offset).getMany();
return new PaginationResult({
first: offset + 1,
last: offset + data.length,
limit: options.limit,
total: options.total ? await qb.getCount() : null,
data,
});
} |
11 - (Practical) Building Full Events API 11-(实用)构建完整事件API001 (Practical) Building Full Events API002 Relations Between Entities 实体之间的关系截断所有表由于开发阶段在不断个更新列名,旧数据的新列名将为空,发生错误,需要清空数据表 SET
foreign_key_checks = 0;
TRUNCATE TABLE `attendee`;
TRUNCATE TABLE `event`;
TRUNCATE TABLE `profile`;
TRUNCATE TABLE `subject`;
TRUNCATE TABLE `subject_teachers_teacher`;
TRUNCATE TABLE `teacher`;
TRUNCATE TABLE `user`;
SET
foreign_key_checks = 1; 这段SQL语句用于清空多个表的内容,并在此过程中暂时禁用外键约束检查,以避免因外键依赖而导致的删除错误。下面是对这个SQL语句的逐步解析: 禁用外键约束检查SET foreign_key_checks = 0;
清空(截断)表接下来的几条 TRUNCATE TABLE `attendee`;
TRUNCATE TABLE `event`;
TRUNCATE TABLE `profile`;
TRUNCATE TABLE `subject`;
TRUNCATE TABLE `subject_teachers_teacher`;
TRUNCATE TABLE `teacher`;
TRUNCATE TABLE `user`;
重新启用外键约束检查SET foreign_key_checks = 1;
使用场景这样的操作通常用于开发或测试环境,在需要快速清空数据库内容并从头开始时非常有用。但是,在生产环境中直接使用这种方法需要非常谨慎,因为它会永久删除所有指定表中的数据,并且不能恢复。 总的来说,这段SQL语句是一种快速清理数据库内容的方法,通过暂时禁用外键约束来避免在清空表时遇到依赖问题。在执行这类操作时,建议确保已经做好了适当的数据备份,以防不测。 用户与出席者 一对多关系
import { Expose } from 'class-transformer';
import { Attendee } from 'src/events/attendee.entity';
import {
Column,
Entity,
JoinColumn,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Event } from './../events/event.entity';
import { Profile } from './profile.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
@Expose()
id: number;
@Column({ unique: true })
@Expose()
username: string;
@Column()
password: string;
@Column({ unique: true })
@Expose()
email: string;
@Column()
@Expose()
firstName: string;
@Column()
@Expose()
lastName: string;
@OneToOne(() => Profile)
@JoinColumn()
@Expose()
profile: Profile;
@OneToMany(() => Event, (event) => event.organizer)
@Expose()
organized: Event[];
@OneToMany(() => Attendee, (attendee) => attendee.user)
attended: Attendee[];
}
import { Expose } from 'class-transformer';
import { User } from 'src/auth/user.entity';
import {
Column,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Event } from './event.entity';
export enum AttendeeAnswerEnum {
Accepted = 1,
Maybe,
Rejected,
}
@Entity()
export class Attendee {
@PrimaryGeneratedColumn()
@Expose()
id: number;
@Column()
@Expose()
name: string;
@ManyToOne(() => Event, (event) => event.attendees, {
nullable: true,
})
@JoinColumn()
event: Event;
@Column()
eventId: number;
@Column('enum', {
enum: AttendeeAnswerEnum,
default: AttendeeAnswerEnum.Accepted,
})
@Expose()
answer: AttendeeAnswerEnum;
@ManyToOne(() => User, (user) => user.attended)
user: User;
@Column()
userId: number;
} 事件实体定义通用分页类型 PaginatedEvents
import { Expose } from 'class-transformer';
import { User } from 'src/auth/user.entity';
import { PaginationResult } from 'src/pagination/paginator';
import {
Column,
Entity,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Attendee } from './attendee.entity';
@Entity()
export class Event {
@PrimaryGeneratedColumn()
@Expose()
id: number;
@Column()
@Expose()
name: string;
@Column()
@Expose()
description: string;
@Column()
@Expose()
when: Date;
@Column()
@Expose()
address: string;
@OneToMany(() => Attendee, (attendee) => attendee.event, {
cascade: true,
})
@Expose()
attendees: Attendee[];
@ManyToOne(() => User, (user) => user.organized)
@Expose()
organizer: User;
@Column({ nullable: true })
organizerId: number;
@Expose()
attendeeCount?: number;
@Expose()
attendeeRejected?: number;
@Expose()
attendeeMaybe?: number;
@Expose()
attendeeAccepted?: number;
}
export type PaginatedEvents = PaginationResult<Event>; 003 Getting Event Attendees 获取活动参与者控制器只应该获取用户输入,然后响应。 创建出席者服务获取所有参与指定事件的出席者 这里Attendee实体中有一个名为event的关联属性,该属性链接到一个事件实体,而这个事件实体有一个id字段。
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Attendee } from './attendee.entity';
export class AttendeesService {
constructor(
@InjectRepository(Attendee)
private readonly attendeeRepository: Repository<Attendee>,
) {}
public async findByEventId(eventId: number): Promise<Attendee[]> {
return await this.attendeeRepository.find({
where: { event: { id: eventId } },
});
}
} 会议事件出席者控制器
import {
ClassSerializerInterceptor,
Controller,
Get,
Param,
SerializeOptions,
UseInterceptors,
} from '@nestjs/common';
import { AttendeesService } from './attendees.service';
@Controller('events/:eventId/attendees')
@SerializeOptions({ strategy: 'excludeAll' })
export class EventAttendeesController {
constructor(private readonly attendeesService: AttendeesService) {}
@Get()
@UseInterceptors(ClassSerializerInterceptor)
async findAll(@Param('eventId') eventId: number) {
return await this.attendeesService.findByEventId(eventId);
}
} 004 Getting Events Organized by User 获取按用户组织的事件
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/auth/user.entity';
import { paginate, PaginateOptions } from 'src/pagination/paginator';
import { DeleteResult, Repository } from 'typeorm';
import { AttendeeAnswerEnum } from './attendee.entity';
import { Event, PaginatedEvents } from './event.entity';
import { CreateEventDto } from './input/create-event.dto';
import { ListEvents, WhenEventFilter } from './input/list.events';
import { UpdateEventDto } from './input/update-event.dto';
@Injectable()
export class EventsService {
private readonly logger = new Logger(EventsService.name);
constructor(
@InjectRepository(Event)
private readonly eventsRepository: Repository<Event>,
) {}
private getEventsBaseQuery() {
return this.eventsRepository
.createQueryBuilder('e')
.orderBy('e.id', 'DESC');
}
public getEventsWithAttendeeCountQuery() {
return this.getEventsBaseQuery()
.loadRelationCountAndMap('e.attendeeCount', 'e.attendees')
.loadRelationCountAndMap(
'e.attendeeAccepted',
'e.attendees',
'attendee',
(qb) =>
qb.where('attendee.answer = :answer', {
answer: AttendeeAnswerEnum.Accepted,
}),
)
.loadRelationCountAndMap(
'e.attendeeMaybe',
'e.attendees',
'attendee',
(qb) =>
qb.where('attendee.answer = :answer', {
answer: AttendeeAnswerEnum.Maybe,
}),
)
.loadRelationCountAndMap(
'e.attendeeRejected',
'e.attendees',
'attendee',
(qb) =>
qb.where('attendee.answer = :answer', {
answer: AttendeeAnswerEnum.Rejected,
}),
);
}
private async getEventsWithAttendeeCountFiltered(filter?: ListEvents) {
let query = this.getEventsWithAttendeeCountQuery();
if (!filter) {
return query;
}
if (filter.when) {
if (filter.when == WhenEventFilter.Today) {
query = query.andWhere(
`e.when >= CURDATE() AND e.when <= CURDATE() + INTERVAL 1 DAY`,
);
}
if (filter.when == WhenEventFilter.Tommorow) {
query = query.andWhere(
`e.when >= CURDATE() + INTERVAL 1 DAY AND e.when <= CURDATE() + INTERVAL 2 DAY`,
);
}
if (filter.when == WhenEventFilter.ThisWeek) {
query = query.andWhere('YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1)');
}
if (filter.when == WhenEventFilter.NextWeek) {
query = query.andWhere(
'YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1) + 1',
);
}
}
return query;
}
public async getEventsWithAttendeeCountFilteredPaginated(
filter: ListEvents,
paginateOptions: PaginateOptions,
): Promise<PaginatedEvents> {
return await paginate(
await this.getEventsWithAttendeeCountFiltered(filter),
paginateOptions,
);
}
public async getEvent(id: number): Promise<Event | undefined> {
const query = this.getEventsWithAttendeeCountQuery().andWhere(
'e.id = :id',
{ id },
);
this.logger.debug(query.getSql());
return await query.getOne();
}
public async createEvent(input: CreateEventDto, user: User): Promise<Event> {
return await this.eventsRepository.save({
...input,
organizer: user,
when: new Date(input.when),
});
}
public async updateEvent(
event: Event,
input: UpdateEventDto,
): Promise<Event> {
return await this.eventsRepository.save({
...event,
...input,
when: input.when ? new Date(input.when) : event.when,
});
}
public async deleteEvent(id: number): Promise<DeleteResult> {
return await this.eventsRepository
.createQueryBuilder('e')
.delete()
.where('id = :id', { id })
.execute();
}
public async getEventsOrganizedByUserIdPaginated(
userId: number,
paginateOptions: PaginateOptions,
): Promise<PaginatedEvents> {
return await paginate<Event>(
this.getEventsOrganizedByUserIdQuery(userId),
paginateOptions,
);
}
private getEventsOrganizedByUserIdQuery(userId: number) {
return this.getEventsBaseQuery().where('e.organizerId = :userId', {
userId,
});
}
} 创建事件组织者控制器
import {
ClassSerializerInterceptor,
Controller,
Get,
Param,
Query,
SerializeOptions,
UseInterceptors,
} from '@nestjs/common';
import { EventsService } from './events.service';
@Controller('events-organized-by-user/:userId')
@SerializeOptions({ strategy: 'excludeAll' })
export class EventsOrganizedByUserController {
constructor(private readonly eventsService: EventsService) {}
@Get()
@UseInterceptors(ClassSerializerInterceptor)
async findAll(@Param('userId') userId: number, @Query('page') page = 1) {
return await this.eventsService.getEventsOrganizedByUserIdPaginated(
userId,
{ currentPage: page, limit: 5 },
);
}
} 事件模块注册 出席者服务为提供者
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Attendee } from './attendee.entity';
import { AttendeesService } from './attendees.service';
import { Event } from './event.entity';
import { EventsController } from './events.controller';
import { EventsService } from './events.service';
@Module({
imports: [TypeOrmModule.forFeature([Event, Attendee])],
controllers: [EventsController],
providers: [EventsService, AttendeesService],
})
export class EventsModule {} 005 Current User Event Attendance - the Business Logic 当前用户事件出席者-业务逻辑DTO
import { IsEnum } from 'class-validator';
import { AttendeeAnswerEnum } from './../attendee.entity';
export class CreateAttendeeDto {
@IsEnum(AttendeeAnswerEnum)
answer: AttendeeAnswerEnum;
} 服务
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Attendee } from './attendee.entity';
import { CreateAttendeeDto } from './input/create-attendee.dto';
@Injectable()
export class AttendeesService {
constructor(
@InjectRepository(Attendee)
private readonly attendeeRepository: Repository<Attendee>,
) {}
public async findByEventId(eventId: number): Promise<Attendee[]> {
return await this.attendeeRepository.find({
where: {
event: { id: eventId },
},
});
}
public async findOneByEventIdAndUserId(
eventId: number,
userId: number,
): Promise<Attendee | undefined> {
return await this.attendeeRepository.findOne({
where: {
event: { id: eventId },
user: { id: userId },
},
});
}
public async createOrUpdate(
input: CreateAttendeeDto,
eventId: number,
userId: number,
): Promise<Attendee> {
const attendee =
(await this.findOneByEventIdAndUserId(eventId, userId)) ?? new Attendee();
attendee.eventId = eventId;
attendee.userId = userId;
attendee.answer = input.answer;
return await this.attendeeRepository.save(attendee);
}
}
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/auth/user.entity';
import { paginate, PaginateOptions } from 'src/pagination/paginator';
import { DeleteResult, Repository } from 'typeorm';
import { AttendeeAnswerEnum } from './attendee.entity';
import { Event, PaginatedEvents } from './event.entity';
import { CreateEventDto } from './input/create-event.dto';
import { ListEvents, WhenEventFilter } from './input/list.events';
import { UpdateEventDto } from './input/update-event.dto';
@Injectable()
export class EventsService {
private readonly logger = new Logger(EventsService.name);
constructor(
@InjectRepository(Event)
private readonly eventsRepository: Repository<Event>,
) {}
private getEventsBaseQuery() {
return this.eventsRepository
.createQueryBuilder('e')
.orderBy('e.id', 'DESC');
}
public getEventsWithAttendeeCountQuery() {
return this.getEventsBaseQuery()
.loadRelationCountAndMap('e.attendeeCount', 'e.attendees')
.loadRelationCountAndMap(
'e.attendeeAccepted',
'e.attendees',
'attendee',
(qb) =>
qb.where('attendee.answer = :answer', {
answer: AttendeeAnswerEnum.Accepted,
}),
)
.loadRelationCountAndMap(
'e.attendeeMaybe',
'e.attendees',
'attendee',
(qb) =>
qb.where('attendee.answer = :answer', {
answer: AttendeeAnswerEnum.Maybe,
}),
)
.loadRelationCountAndMap(
'e.attendeeRejected',
'e.attendees',
'attendee',
(qb) =>
qb.where('attendee.answer = :answer', {
answer: AttendeeAnswerEnum.Rejected,
}),
);
}
private async getEventsWithAttendeeCountFiltered(filter?: ListEvents) {
let query = this.getEventsWithAttendeeCountQuery();
if (!filter) {
return query;
}
if (filter.when) {
if (filter.when == WhenEventFilter.Today) {
query = query.andWhere(
`e.when >= CURDATE() AND e.when <= CURDATE() + INTERVAL 1 DAY`,
);
}
if (filter.when == WhenEventFilter.Tommorow) {
query = query.andWhere(
`e.when >= CURDATE() + INTERVAL 1 DAY AND e.when <= CURDATE() + INTERVAL 2 DAY`,
);
}
if (filter.when == WhenEventFilter.ThisWeek) {
query = query.andWhere('YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1)');
}
if (filter.when == WhenEventFilter.NextWeek) {
query = query.andWhere(
'YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1) + 1',
);
}
}
return query;
}
public async getEventsWithAttendeeCountFilteredPaginated(
filter: ListEvents,
paginateOptions: PaginateOptions,
): Promise<PaginatedEvents> {
return await paginate(
await this.getEventsWithAttendeeCountFiltered(filter),
paginateOptions,
);
}
public async getEvent(id: number): Promise<Event | undefined> {
const query = this.getEventsWithAttendeeCountQuery().andWhere(
'e.id = :id',
{ id },
);
this.logger.debug(query.getSql());
return await query.getOne();
}
public async createEvent(input: CreateEventDto, user: User): Promise<Event> {
return await this.eventsRepository.save({
...input,
organizer: user,
when: new Date(input.when),
});
}
public async updateEvent(
event: Event,
input: UpdateEventDto,
): Promise<Event> {
return await this.eventsRepository.save({
...event,
...input,
when: input.when ? new Date(input.when) : event.when,
});
}
public async deleteEvent(id: number): Promise<DeleteResult> {
return await this.eventsRepository
.createQueryBuilder('e')
.delete()
.where('id = :id', { id })
.execute();
}
public async getEventsOrganizedByUserIdPaginated(
userId: number,
paginateOptions: PaginateOptions,
): Promise<PaginatedEvents> {
return await paginate<Event>(
this.getEventsOrganizedByUserIdQuery(userId),
paginateOptions,
);
}
private getEventsOrganizedByUserIdQuery(userId: number) {
return this.getEventsBaseQuery().where('e.organizerId = :userId', {
userId,
});
}
public async getEventsAttendedByUserIdPaginated(
userId: number,
paginateOptions: PaginateOptions,
): Promise<PaginatedEvents> {
return await paginate<Event>(
this.getEventsAttendedByUserIdQuery(userId),
paginateOptions,
);
}
private getEventsAttendedByUserIdQuery(userId: number) {
return this.getEventsBaseQuery()
.leftJoinAndSelect('e.attendees', 'a')
.where('a.userId = :userId', { userId });
}
} 006 Current User Event Attendance - the Controller
import {
Body,
ClassSerializerInterceptor,
Controller,
Get,
NotFoundException,
Param,
ParseIntPipe,
Put,
Query,
SerializeOptions,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { CurrentUser } from 'src/auth/current-user.decorator';
import { User } from 'src/auth/user.entity';
import { AuthGuardJwt } from './../auth/auth-guard.jwt';
import { AttendeesService } from './attendees.service';
import { EventsService } from './events.service';
import { CreateAttendeeDto } from './input/create-attendee.dto';
@Controller('events-attendance')
@SerializeOptions({ strategy: 'excludeAll' })
export class CurrentUserEventAttendanceController {
constructor(
private readonly eventsService: EventsService,
private readonly attendeesService: AttendeesService,
) {}
@Get()
@UseGuards(AuthGuardJwt)
@UseInterceptors(ClassSerializerInterceptor)
async findAll(@CurrentUser() user: User, @Query('page') page = 1) {
return await this.eventsService.getEventsAttendedByUserIdPaginated(
user.id,
{ limit: 6, currentPage: page },
);
}
@Get(':/eventId')
@UseGuards(AuthGuardJwt)
@UseInterceptors(ClassSerializerInterceptor)
async findOne(
@Param('eventId', ParseIntPipe) eventId: number,
@CurrentUser() user: User,
) {
const attendee = await this.attendeesService.findOneByEventIdAndUserId(
eventId,
user.id,
);
if (!attendee) {
throw new NotFoundException();
}
return attendee;
}
@Put('/:eventId')
@UseGuards(AuthGuardJwt)
@UseInterceptors(ClassSerializerInterceptor)
async createOrUpdate(
@Param('eventId', ParseIntPipe) eventId: number,
@Body() input: CreateAttendeeDto,
@CurrentUser() user: User,
) {
return this.attendeesService.createOrUpdate(input, eventId, user.id);
}
} 007 Events Refactoring用户和出席者不能全部公开,否则会导致循环引用。 只公开出席者中的user.
import {
Body,
ClassSerializerInterceptor,
Controller,
DefaultValuePipe,
Get,
NotFoundException,
Param,
ParseIntPipe,
Put,
Query,
SerializeOptions,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { CurrentUser } from 'src/auth/current-user.decorator';
import { User } from 'src/auth/user.entity';
import { AuthGuardJwt } from './../auth/auth-guard.jwt';
import { AttendeesService } from './attendees.service';
import { EventsService } from './events.service';
import { CreateAttendeeDto } from './input/create-attendee.dto';
@Controller('events-attendance')
@SerializeOptions({ strategy: 'excludeAll' })
export class CurrentUserEventAttendanceController {
constructor(
private readonly eventsService: EventsService,
private readonly attendeesService: AttendeesService,
) {}
@Get()
@UseGuards(AuthGuardJwt)
@UseInterceptors(ClassSerializerInterceptor)
async findAll(
@CurrentUser() user: User,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page = 1,
) {
return await this.eventsService.getEventsAttendedByUserIdPaginated(
user.id,
{ limit: 6, currentPage: page },
);
}
@Get(':eventId')
@UseGuards(AuthGuardJwt)
@UseInterceptors(ClassSerializerInterceptor)
async findOne(
@Param('eventId', ParseIntPipe) eventId: number,
@CurrentUser() user: User,
) {
const attendee = await this.attendeesService.findOneByEventIdAndUserId(
eventId,
user.id,
);
if (!attendee) {
throw new NotFoundException();
}
return attendee;
}
@Put('/:eventId')
@UseGuards(AuthGuardJwt)
@UseInterceptors(ClassSerializerInterceptor)
async createOrUpdate(
@Param('eventId', ParseIntPipe) eventId: number,
@Body() input: CreateAttendeeDto,
@CurrentUser() user: User,
) {
return this.attendeesService.createOrUpdate(input, eventId, user.id);
}
}
import { Expose } from 'class-transformer';
import { User } from 'src/auth/user.entity';
import { PaginationResult } from 'src/pagination/paginator';
import {
Column,
Entity,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Attendee } from './attendee.entity';
@Entity()
export class Event {
constructor(partial?: Partial<Event>) {
Object.assign(this, partial);
}
@PrimaryGeneratedColumn()
@Expose()
id: number;
@Column()
@Expose()
name: string;
@Column()
@Expose()
description: string;
@Column()
@Expose()
when: Date;
@Column()
@Expose()
address: string;
@OneToMany(() => Attendee, (attendee) => attendee.event, {
cascade: true,
})
@Expose()
attendees: Attendee[];
@ManyToOne(() => User, (user) => user.organized)
@Expose()
organizer: User;
@Column({ nullable: true })
organizerId: number;
@Expose()
attendeeCount?: number;
@Expose()
attendeeRejected?: number;
@Expose()
attendeeMaybe?: number;
@Expose()
attendeeAccepted?: number;
}
export type PaginatedEvents = PaginationResult<Event>;
import {
ClassSerializerInterceptor,
Controller,
DefaultValuePipe,
Get,
Param,
ParseIntPipe,
Query,
SerializeOptions,
UseInterceptors,
} from '@nestjs/common';
import { EventsService } from './events.service';
@Controller('events-organized-by-user/:userId')
@SerializeOptions({ strategy: 'excludeAll' })
export class EventsOrganizedByUserController {
constructor(private readonly eventsService: EventsService) {}
@Get()
@UseInterceptors(ClassSerializerInterceptor)
async findAll(
@Param('userId', ParseIntPipe) userId: number,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page = 1,
) {
return await this.eventsService.getEventsOrganizedByUserIdPaginated(
userId,
{ currentPage: page, limit: 5 },
);
}
}
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/auth/user.entity';
import { paginate, PaginateOptions } from 'src/pagination/paginator';
import { DeleteResult, Repository, SelectQueryBuilder } from 'typeorm';
import { AttendeeAnswerEnum } from './attendee.entity';
import { Event, PaginatedEvents } from './event.entity';
import { CreateEventDto } from './input/create-event.dto';
import { ListEvents, WhenEventFilter } from './input/list.events';
import { UpdateEventDto } from './input/update-event.dto';
@Injectable()
export class EventsService {
private readonly logger = new Logger(EventsService.name);
constructor(
@InjectRepository(Event)
private readonly eventsRepository: Repository<Event>,
) {}
private getEventsBaseQuery(): SelectQueryBuilder<Event> {
return this.eventsRepository
.createQueryBuilder('e')
.orderBy('e.id', 'DESC');
}
public getEventsWithAttendeeCountQuery(): SelectQueryBuilder<Event> {
return this.getEventsBaseQuery()
.loadRelationCountAndMap('e.attendeeCount', 'e.attendees')
.loadRelationCountAndMap(
'e.attendeeAccepted',
'e.attendees',
'attendee',
(qb) =>
qb.where('attendee.answer = :answer', {
answer: AttendeeAnswerEnum.Accepted,
}),
)
.loadRelationCountAndMap(
'e.attendeeMaybe',
'e.attendees',
'attendee',
(qb) =>
qb.where('attendee.answer = :answer', {
answer: AttendeeAnswerEnum.Maybe,
}),
)
.loadRelationCountAndMap(
'e.attendeeRejected',
'e.attendees',
'attendee',
(qb) =>
qb.where('attendee.answer = :answer', {
answer: AttendeeAnswerEnum.Rejected,
}),
);
}
private getEventsWithAttendeeCountFilteredQuery(
filter?: ListEvents,
): SelectQueryBuilder<Event> {
let query = this.getEventsWithAttendeeCountQuery();
if (!filter) {
return query;
}
if (filter.when) {
if (filter.when == WhenEventFilter.Today) {
query = query.andWhere(
`e.when >= CURDATE() AND e.when <= CURDATE() + INTERVAL 1 DAY`,
);
}
if (filter.when == WhenEventFilter.Tommorow) {
query = query.andWhere(
`e.when >= CURDATE() + INTERVAL 1 DAY AND e.when <= CURDATE() + INTERVAL 2 DAY`,
);
}
if (filter.when == WhenEventFilter.ThisWeek) {
query = query.andWhere('YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1)');
}
if (filter.when == WhenEventFilter.NextWeek) {
query = query.andWhere(
'YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1) + 1',
);
}
}
return query;
}
public async getEventsWithAttendeeCountFilteredPaginated(
filter: ListEvents,
paginateOptions: PaginateOptions,
): Promise<PaginatedEvents> {
return await paginate(
await this.getEventsWithAttendeeCountFilteredQuery(filter),
paginateOptions,
);
}
public async getEventWithAttendeeCount(
id: number,
): Promise<Event | undefined> {
const query = this.getEventsWithAttendeeCountQuery().andWhere(
'e.id = :id',
{ id },
);
this.logger.debug(query.getSql());
return await query.getOne();
}
public async findOne(id: number): Promise<Event | undefined> {
return await this.eventsRepository.findOne({ where: { id } });
}
public async createEvent(input: CreateEventDto, user: User): Promise<Event> {
return await this.eventsRepository.save(
new Event({
...input,
organizer: user,
when: new Date(input.when),
}),
);
}
public async updateEvent(
event: Event,
input: UpdateEventDto,
): Promise<Event> {
return await this.eventsRepository.save(
new Event({
...event,
...input,
when: input.when ? new Date(input.when) : event.when,
}),
);
}
public async deleteEvent(id: number): Promise<DeleteResult> {
return await this.eventsRepository
.createQueryBuilder('e')
.delete()
.where('id = :id', { id })
.execute();
}
public async getEventsOrganizedByUserIdPaginated(
userId: number,
paginateOptions: PaginateOptions,
): Promise<PaginatedEvents> {
return await paginate<Event>(
this.getEventsOrganizedByUserIdQuery(userId),
paginateOptions,
);
}
private getEventsOrganizedByUserIdQuery(
userId: number,
): SelectQueryBuilder<Event> {
return this.getEventsBaseQuery().where('e.organizerId = :userId', {
userId,
});
}
public async getEventsAttendedByUserIdPaginated(
userId: number,
paginateOptions: PaginateOptions,
): Promise<PaginatedEvents> {
return await paginate<Event>(
this.getEventsAttendedByUserIdQuery(userId),
paginateOptions,
);
}
private getEventsAttendedByUserIdQuery(
userId: number,
): SelectQueryBuilder<Event> {
return this.getEventsBaseQuery()
.leftJoinAndSelect('e.attendees', 'a')
.where('a.userId = :userId', { userId });
}
}
import {
Body,
ClassSerializerInterceptor,
Controller,
Delete,
ForbiddenException,
Get,
HttpCode,
Logger,
NotFoundException,
Param,
ParseIntPipe,
Patch,
Post,
Query,
SerializeOptions,
UseGuards,
UseInterceptors,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { AuthGuardJwt } from 'src/auth/auth-guard.jwt';
import { CurrentUser } from 'src/auth/current-user.decorator';
import { User } from 'src/auth/user.entity';
import { EventsService } from './events.service';
import { CreateEventDto } from './input/create-event.dto';
import { ListEvents } from './input/list.events';
import { UpdateEventDto } from './input/update-event.dto';
@Controller('/events')
@SerializeOptions({ strategy: 'excludeAll' })
export class EventsController {
private readonly logger = new Logger(EventsController.name);
constructor(private readonly eventsService: EventsService) {}
@Get()
@UsePipes(new ValidationPipe({ transform: true }))
@UseInterceptors(ClassSerializerInterceptor)
async findAll(@Query() filter: ListEvents) {
const events =
await this.eventsService.getEventsWithAttendeeCountFilteredPaginated(
filter,
{
total: true,
currentPage: filter.page,
limit: 2,
},
);
return events;
}
// @Get('/practice')
// async practice() {
// // return await this.repository.find({
// // select: ['id', 'when'],
// // where: [{
// // id: MoreThan(3),
// // when: MoreThan(new Date('2021-02-12T13:00:00'))
// // }, {
// // description: Like('%meet%')
// // }],
// // take: 2,
// // order: {
// // id: 'DESC'
// // }
// // });
// }
// @Get('practice2')
// async practice2() {
// // // return await this.repository.findOne(
// // // 1,
// // // { relations: ['attendees'] }
// // // );
// // const event = await this.repository.findOne(
// // 1,
// // { relations: ['attendees'] }
// // );
// // // const event = new Event();
// // // event.id = 1;
// // const attendee = new Attendee();
// // attendee.name = 'Using cascade';
// // // attendee.event = event;
// // event.attendees.push(attendee);
// // // event.attendees = [];
// // // await this.attendeeRepository.save(attendee);
// // await this.repository.save(event);
// // return event;
// // return await this.repository.createQueryBuilder('e')
// // .select(['e.id', 'e.name'])
// // .orderBy('e.id', 'ASC')
// // .take(3)
// // .getMany();
// }
@Get(':id')
@UseInterceptors(ClassSerializerInterceptor)
async findOne(@Param('id', ParseIntPipe) id: number) {
// console.log(typeof id);
const event = await this.eventsService.getEventWithAttendeeCount(id);
if (!event) {
throw new NotFoundException();
}
return event;
}
// You can also use the @UsePipes decorator to enable pipes.
// It can be done per method, or for every method when you
// add it at the controller level.
@Post()
@UseGuards(AuthGuardJwt)
@UseInterceptors(ClassSerializerInterceptor)
async create(@Body() input: CreateEventDto, @CurrentUser() user: User) {
return await this.eventsService.createEvent(input, user);
}
// Create new ValidationPipe to specify validation group inside @Body
// new ValidationPipe({ groups: ['update'] })
@Patch(':id')
@UseGuards(AuthGuardJwt)
@UseInterceptors(ClassSerializerInterceptor)
async update(
@Param('id', ParseIntPipe) id,
@Body() input: UpdateEventDto,
@CurrentUser() user: User,
) {
const event = await this.eventsService.findOne(id);
if (!event) {
throw new NotFoundException();
}
if (event.organizerId !== user.id) {
throw new ForbiddenException(
null,
`You are not authorized to change this event`,
);
}
return await this.eventsService.updateEvent(event, input);
}
@Delete(':id')
@UseGuards(AuthGuardJwt)
@HttpCode(204)
async remove(@Param('id', ParseIntPipe) id, @CurrentUser() user: User) {
const event = await this.eventsService.findOne(id);
if (!event) {
throw new NotFoundException();
}
if (event.organizerId !== user.id) {
throw new ForbiddenException(
null,
`You are not authorized to remove this event`,
);
}
await this.eventsService.deleteEvent(id);
}
}
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Attendee } from './attendee.entity';
import { AttendeesService } from './attendees.service';
import { CurrentUserEventAttendanceController } from './current-user-event-attendance.controller';
import { EventAttendeesController } from './event-attendees.controller';
import { Event } from './event.entity';
import { EventsOrganizedByUserController } from './events-organized-by-user.controller';
import { EventsController } from './events.controller';
import { EventsService } from './events.service';
@Module({
imports: [TypeOrmModule.forFeature([Event, Attendee])],
controllers: [
EventsController,
CurrentUserEventAttendanceController,
EventAttendeesController,
EventsOrganizedByUserController,
],
providers: [EventsService, AttendeesService],
})
export class EventsModule {} |
12 - Introduction to Testing (ManualAutomatic) 测试简介(手动-自动) |
精通 NestJS – Node.js 框架 Master NestJS 9 - Node.js Framework
https://www.udemy.com/course/master-nestjs-the-javascript-nodejs-framework/
学习 Nest 9 和 Node、GraphQL、REST、单元测试、E2E 测试、Type ORM 3、使用 TypeScript 进行 API 开发等等!
开发健壮的 REST API
理解和创建 GraphQL API
单元测试和端到端测试
使用 Docker 的稳健开发工作流程
使用现代数据库抽象 (TypeORM)
了解模块、供应商和服务!
学习身份验证和授权(使用 Passport)
了解 JWT 令牌的工作原理
了解如何配置应用程序以及如何保留日志
了解查询生成器 – 了解如何有效地构建查询
了解如何验证和序列化数据
学习使用 Nest CLI
了解代码设计模式,例如存储库或服务
需要VSCode
VSCode插件:
JavaScript and TypeScript Nightly
JavaScript and TypeScript Nightly - Visual Studio Marketplace
TypeScript Importer
https://marketplace.visualstudio.com/items?itemName=pmneo.tsimporter
postman
https://www.postman.com/downloads/
项目源码
https://github.com/piotr-jura-udemy/master-nest-js
The text was updated successfully, but these errors were encountered: