Open WangShuXian6 opened 9 months ago
npm 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
需要结束node
taskkill /F /IM node.exe
项目名为 nest-events-backend
nest new nest-events-backend
启动开发服务器
cd nest-events-backend
npm run start:dev
通过 localhost:3000 访问后端服务 默认返回 hello world!
Nest应用程序由模块组成,默认情况下总是至少有一个主模块,称为应用程序模块。 该模块由控制器、服务、实体和其他较小的构建块组成。 模型被定义在他们自己的模块文件中,类装饰器模块被用来描述它们。
bootstrap函数负责创建新的nest应用程序对象 并启动服务器,使其在指定的位置监听,默认情况下是3000。
Module 模块是一个抽象的概念。包含一组功能。 模块是一个用模块装饰器注释的类。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
控制器定义API和端点
服务是一个类,应用程序的业务逻辑放在其中, 业务逻辑不是直接连接到处理一个请求或发送一个响应中。
控制器测试放在控制器旁边
路由基本上是定义了路径,URL,HTTP动词,状态码。
控制器是一个带有控制器装饰器注释的类。 控制器的工作是使用特定的HTTP动词在你的应用程序中创建端点。 然后接受请求,将该请求的处理传递给你项目中的其他代码. 然后返回,将响应返回给客户。 它控制着我们处理请求的过程。
你需要告诉应用程序如何将一个路径转化为控制器和它们的动作。
/events
路径
方式1:@Controller('/events')
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() 空路由 / @Get() 空路由 /
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() 表示根路径的请求响应
访问 localhost:3000
返回 ·this.appService.getHello();· 即 ·Hello World!·
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')
get 请求localhost:3000/bye
返回 Bye
nest genarate controller nset g co
src/events.controller.ts
import { Controller, Delete, Get, Patch, Post } from '@nestjs/common';
@Controller('/events')
export class EventsController {
@Get()
findAll() {}
@Get()
findOne() {}
@Post()
create() {}
@Patch()
update() {}
@Delete()
remove() {}
}
controllers: [AppController, EventsController]
src/app.module.ts
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 都将响应 200
围绕着资源建立的控制器,尽量保持简短,并且最多拥有五个基本的操作, 如果你觉得有必要增加更多的操作,也许你只是需要一个新的资源。 用描述性的方式来调用你的操作是一个好主意。
资源并不总是指我们的数据库表,它可以是更抽象的东西。
这需要定义动态路由
通过参数装饰器 @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) {}
}
json请求体通过@Body
装饰器转换程js对象 input
@Post()
create(@Body() input) {
// json请求体通过@Body转换程js对象 input
return input;
}
src\events.controller.ts
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) {}
}
响应有两种情况
1-返回一个原始值,如字符串。数字或布尔,字面上的东西被返回
2-返回一个数组或一个对象,它被序列化为json
src\events.controller.ts
@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) {}
}
响应 Content-Type
为 application/json; charset=utf-8
默认get成功的的响应状态码为200 get localhost:3000/events
创建资源成功状态码为201 post localhost:3000/events
@Post()
create(@Body() input) {
// json请求体通过@Body转换程js对象 input
return input;
}
更新资源成功状态码为200 patch localhost:3000/events/1
良好的rest API实践是返回刚刚创建或更新的结果。
删除该资源时,不应该返回被删除的资源或任何类型的消息说它被删除了。
最好的做法是不返回任何东西。
删除资源成功状态码为204 @HttpCode(204)
src\events.controller.ts
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) {}
}
状态码
create-event.dto.ts
export class CreateEventDto {
name: string;
description: string;
when: string;
address: string;
}
src\events.controller.ts
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) {}
}
改变一个事件的一些属性,但不发送不需要改变的属性。
继承并将创建资源的DTO的属性改为可选类型
npm i -S @nestjs/mapped-types
src/update-event.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateEventDto } from './create-event.dto';
export class UpdateEventDto extends PartialType(CreateEventDto) {}
src/events.controller.ts
@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) {}
}
完整的事件控制器,它能实际工作的API,可以返回事件和创建、编辑和删除资源。
src/event.entity.ts
export class Event {
id: number;
name: string;
description: string;
when: Date;
address: string;
}
src/events.controller.ts
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));
}
}
docker-compose.yml
version: "3.8"
services:
mysql:
image: mysql:8.0.23
# 容器启动时运行的命令:启用密码认证
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
MYSQL_ROOT_PASSWORD: example
ports:
#将主机的端口3307映射到容器的端口3306
- 3307:3306
postgres:
image: postgres:13.1
restart: always
environment:
POSTGRES_PASSWORD: example
ports:
- 5432:5432
# 数据库管理平台
adminer:
image: adminer
restart: always
ports:
- 8080:8080
运行容器
docker-compose up -d
打开 localhost:8080 创建mysql类型数据库 默认 localhost
系统:MySQL 服务器: 127.0.0.1:3307 用户名:root 密码:example 数据库:留空
服务器为 hostname[:port]
adminer 存在bug,在windows上测试无法使用,无法连接任意种类的数据库。
可以使用 navicat 连接数据库。
mysql
服务器:localhost:3307 用户名:root 密码:example
PostgreSQL 服务器:localhost:5432 初始数据库:postgres 用户名:postgres 密码:example
字符集:utf8mb4
编码:UTF8
ORM代表对象关系映射,它背后的概念是,在面向对象的编程语言中,可以用对象来代表数据库表。 这是一个存在于所有编程语言中的共同概念,它使数据库的工作变得更加容易。 因为你在大多数时候不必手动编写数据库查询。 只在需要非常高效的查询时才手动编写查询语句。
每个Entity 实体类 映射到数据库的一个表。
Repository 存储库是一种编程模式,它为特定的实体提供数据访问,你将使用存储库来加载和存储数据库中的数据。
Query Builder 查询生成器用于构造数据库查询,通过调用特定的方法,以面向对象的方式,创建可重复使用的查询。
npm i -S @nestjs/typeorm typeorm mysql
src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { EventsController } from './events.controller';
@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',
}),
],
controllers: [AppController, EventsController],
providers: [AppService],
})
export class AppModule {}
TypeOrmModule 模块初始化成功,表示已连接到数据库
[Nest] 12164 - 2024/02/21 20:47:57 LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +10ms
@Entity()
@Entity()
会依据类名生成默认的表名
自定义表名:
@Entity('event')
或
@Entity('event', { name: 'event' })
每个实体都必须有一个主键 @PrimaryGeneratedColumn()
自动生成id
在MySQL中是自增整数 ,在postcrisis中是serial,在Oracle中是sequence。
@PrimaryGeneratedColumn('uuid')
@PrimaryGeneratedColumn("rowid")
@PrimaryGeneratedColumn()
id: number;
或者使用已存在的id列作为主键,例如信用卡号,身份证号,手机号 @PrimaryColumn()
或者使用多个@PrimaryColumn()
创建复合主键
src/event.entity.ts
@PrimaryColumn()
id: number;
@PrimaryColumn()
name: string;
其他列必须使用列装饰器 @Column()
默认情况下,会尝试根据属性类型来猜测列的正确类型,也可以明确指定类型。
但它不会是一个抽象的类型,如数字或字符串。
它必须是数据库引擎的特定类型。
@Column('varchar')
不使用 使用列装饰器 @Column()
的属性将不会存储到数据库中。例如特殊属性。
src/event.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Event {
// 每个实体都必须有一个主键
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
description: string;
@Column()
when: Date;
@Column()
address: string;
}
src/app.module.ts
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';
@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,
}),
],
controllers: [AppController, EventsController],
providers: [AppService],
})
export class AppModule {}
synchronize: true 同步模式,true表示将使用定义的实体自动更新数据库的模式,主要用于本地开发。
如果列的名称或属性更新,数据库的列将直接更新。保留原数据。
如果表名更新,数据库将创建新的表,原表将保留。
实体Entity代表数据库中的行 Repository 存储库 代表数据库中的表,管理表的所有实体。 TypeOrmModule 使用泛型存储库管理表的所有实体的增删改查。
TypeOrmModule 使用特殊存储库定制更复杂的通用查询。
src/app.module.ts
TypeOrmModule.forFeature([Event])
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';
@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]),
],
controllers: [AppController, EventsController],
providers: [AppService],
})
export class AppModule {}
所有的数据库操作都需要使用 repository 操作,应用到数据库中。
constructor(
// 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
@InjectRepository(Event)
private readonly repository: Repository<Event>,
) {}
src/events.controller.ts
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateEventDto } from './create-event.dto';
import { Event } from './event.entity';
import { UpdateEventDto } from './update-event.dto';
@Controller('/events')
export class EventsController {
constructor(
// 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
@InjectRepository(Event)
private readonly repository: Repository<Event>,
) {}
@Get()
async findAll() {
return await this.repository.find();
}
@Get(':id')
async findOne(@Param('id') id) {
return await this.repository.findOne({
where: {
id,
},
});
}
@Post()
async create(@Body() input: CreateEventDto) {
return await this.repository.save({
...input,
when: new Date(input.when),
});
}
@Patch(':id')
async update(@Param('id') id, @Body() 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,
});
}
@Delete(':id')
@HttpCode(204)
async remove(@Param('id') id) {
const event = await this.repository.findOne({
where: {
id,
},
});
await this.repository.remove(event);
}
}
SET NAMES utf8mb4;
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');
src\events.controller.ts
import { Like, MoreThan, Repository } from 'typeorm';
@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 localhost:3000/events/practice
Nest中的验证器管道是一组预定义的验证器。 每个验证器都是检查一件事的函数。 它可能会检查一个电话号码是否正确,它可能会检查一些文本的长度,诸如此类。
管道对数据进行验证,转换,等等。
ParseIntPipe 将请求传入的参数字符串 id 格式化为整数类型
可以使用都好添加多个管道,可以使用 new ParseIntPipe() 为管道添加参数
src\events.controller.ts
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,
},
});
}
安装验证器和转换器
npm i -S class-validator class-transformer
src/events.controller.ts
import {
ValidationPipe,
} from '@nestjs/common';
@Post()
async create(@Body(ValidationPipe) input: CreateEventDto) {
return await this.repository.save({
...input,
when: new Date(input.when),
});
}
src/create-event.dto.ts
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;
}
这将对所有控制器进行验证,如果DTO定义了验证装饰器
src/main.ts
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) {
验证分组 类似标签
删除 src\main.ts
的 app.useGlobalPipes(new ValidationPipe());
src/create-event.dto.ts
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;
}
src\events.controller.ts
创建路由使用 带 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,
});
}
src\events.controller.ts
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),
});
}
在抽象支付服务中,需要处理支付,然后发送支付结果邮件。
应将具体的支付服务例如微信支付,支付宝支付作为独立的服务注入到支付服务中。 具体的微信支付等只需要公开通用支付接口即可。例如Pay().由支付服务调用Pay。 抽象支付服务不需要直到各个具体支付服务的细节。
将发送邮件服务也作为作为独立的服务注入到支付服务中。
这是代码模块化,易于测试。
且具体支付服务可以mock。只测试核心支付服务。
每个模块应当只处理特定的任务。 例如订单模块,电子邮件模块。 各模块有自己的数据库实体,控制器。 各模块输出特定的提供者供其他模块使用。
模块的提供者将注入到其它类中使用。
nest generate module events
src\events\events.module.ts
import { Module } from '@nestjs/common';
@Module({})
export class EventsModule {}
这回自动将 EventsModule 导入到app中
src\app.module.ts
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 {}
src\events\
文件夹下将 TypeOrmModule.forFeature([Event]),
EventsController
从 app模块移入events模块
全局模块只需要连接数据库
src\app.module.ts
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';
@Module({
imports: [
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: [AppService],
})
export class AppModule {}
事件模块定义自己的数据库实体和控制器
src\events\events.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Event } from './event.entity';
import { EventsController } from './events.controller';
@Module({
imports: [TypeOrmModule.forFeature([Event])],
controllers: [EventsController],
})
export class EventsModule {}
每个模块只在自己的上下文中可用。 [TypeOrmModule.forFeature([Event]) 只在 事件模块中可用 因为事件模块不需要其他功能的数据库实体。只需要自身相关的数据库实体。
@Module({
imports: [TypeOrmModule.forFeature([Event])],
controllers: [EventsController],
})
export class EventsModule {}
静态模块功能不变,不需要使用额外参数。例如事件模块。
@Module({
imports: [
EventsModule,
]
})
export class AppModule {}
动态模块需要使用参数来确定运行时功能,例如TypeOrmModule在运行时需要数据库连接参数. 这将创建一个模块实例。
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: '127.0.0.1',
port: 3307,
username: 'root',
password: 'example',
database: 'nest-events',
entities: [Event],
synchronize: true,
}),
],
})
export class AppModule {}
使 AppService 可注入到app中
@Module({
providers: [AppService],
})
export class AppModule {}
@Module({
providers: [
{
provide: AppService,
useClass: AppService,
},
],
})
export class AppModule {}
根据情况动态使用不同的服务作为提供者 例如使用中文提供者,请求根路由将返回中文而非英文
src\app.chinese.service.ts
export class AppChineseService {
constructor() {}
getHello(): string {
return `你好 世界! `;
}
}
app中使用 app.chinese.service 作为提供者的实际类
src\app.module.ts
import { AppChineseService } from './app.chinese.service';
@Module({
providers: [
{
provide: AppService,
//useClass: AppService,
useClass: AppChineseService,
},
],
})
export class AppModule {}
控制器中调用 this.appService.getHello() 将返回中文版本响应文字
src\app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
console.log('hello');
return this.appService.getHello();
}
@Get('/bye')
getbye(): string {
return 'Bye';
}
}
get localhost:3000
只要提供接口 getHello ,任意语言都可以替换提供者。
例如将程序名称和消息的值作为提供者
providers: [
{
provide: AppService,
//useClass: AppService,
useClass: AppChineseService,
},
{
provide: 'APP_NAME',
useValue: 'Nest Events Backend!',
},
],
src\app.module.ts
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';
@Module({
imports: [
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!',
},
],
})
export class AppModule {}
使用 APP_NAME 提供者的值 APP_NAME 的值注入给私有变量 name
src\app.chinese.service.ts
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class AppChineseService {
constructor(
@Inject('APP_NAME')
private readonly name: string,
) {}
getHello(): string {
return `你好 世界! from ${this.name}`;
}
}
工厂函数提供者返回一个值
待注入的类
src\app.dummy.ts
export class AppDummy {
public dummy(): string {
return 'dummy';
}
}
将AppDummy 作为提供者
src\app.module.ts
providers: [
{
provide: 'MESSAGE',
inject: [AppDummy], //将标准提供者AppDummy注入工厂函数
useFactory: (app) => `${app.dummy()} Factory!`, //注入的提供者将成为工厂函数的参数 app = AppDummy
},
AppDummy, //成为标准提供者
],
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';
@Module({
imports: [
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 {}
使用 工厂函数提供者 MESSAGE
src\app.chinese.service.ts
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class AppChineseService {
constructor(
@Inject('APP_NAME')
private readonly name: string,
@Inject('MESSAGE')
private readonly message: string,
) {}
getHello(): string {
return `你好 世界! from ${this.name}, ${this.message}`;
}
}
不同的环境将使用不同的数据库等等。 每个环境需要独立的配置。
npm i -S @nestjs/config
仅在当前模块下有效
ConfigModule.forRoot()
将寻找根目录下 的 .env
,将其中的变量加载到进程中。
src/app.module.ts
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 读取。
例如:
console.log(process.env);
.env
文件配置环境变量不同的环境应当有自己独立的env文件
.env
DB_HOST=localhost
DB_PORT=3307
DB_USER=root
DB_PASSWORD=example
DB_NAME=nest-events
需要重启应用载入新环境变量env文件
src\app.chinese.service.ts
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class AppChineseService {
constructor(
@Inject('APP_NAME')
private readonly name: string,
@Inject('MESSAGE')
private readonly message: string,
) {}
getHello(): string {
console.log(process.env.DB_HOST);
return `你好 世界! from ${this.name}, ${this.message}`;
}
}
src\app.module.ts
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: 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],
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 {}
src\app.module.ts
ConfigModule.forRoot({
isGlobal: true, //全局有效
ignoreEnvFile: false, // 是否加载 env 环境配置文件 例如,如果需要通过docker配置环境变量则true
envFilePath: '.env', //指定将要加载二点环境变量配置文件
}),
自定义配置导出工厂函数 registerAs
src/config/orm.config.ts
import { registerAs } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
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],
synchronize: true,
}),
);
生产环境中停用同步模式 synchronize
src/config/orm.config.prod.ts
import { registerAs } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
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],
synchronize: false,
}),
);
.env
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}
加载环境配置
src\app.module.ts
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';
@Module({
imports: [
// 使用 ormConfig 作为通用环境配置
ConfigModule.forRoot({
isGlobal: true,
load: [ormConfig], //通过配置服务读取配置
expandVariables: true, //启用可扩展环境变量 以支持字符串变量 ${APP_URL}
}),
// 异步加载工厂函数配置 分别用以数据库配置
TypeOrmModule.forRootAsync({
useFactory:
process.env.NODE_ENV !== 'production' ? ormConfig : ormConfigProd,
}),
EventsModule,
],
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 {}
src\events\events.controller.ts
import {
Logger
} from '@nestjs/common';
@Controller('/events')
export class EventsController {
private readonly logger = new Logger(EventsController.name);
constructor(
// 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
@InjectRepository(Event)
private readonly repository: Repository<Event>,
) {}
@Get()
async findAll() {
this.logger.log(`Hit the findAll route`);
const events = await this.repository.find();
this.logger.debug(`Found ${events.length} events`);
return events;
}
}
程序只输出 'error', 'warn', 'debug' 级别的日志,普通级别日志将被忽略。 不需要再手动为生产环境删除日志代码
src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'debug'],
});
// Remove line below to enable local ValidationPipe settings
// 全局启用验证器
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
查找,更新,删除资源时,如果资源不存在。则返回异常
src/events/events.controller.ts
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
ParseIntPipe,
Logger,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Like, MoreThan, Repository } from 'typeorm';
import { CreateEventDto } from './create-event.dto';
import { Event } from './event.entity';
import { UpdateEventDto } from './update-event.dto';
@Controller('/events')
export class EventsController {
private readonly logger = new Logger(EventsController.name);
constructor(
// 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
@InjectRepository(Event)
private readonly repository: Repository<Event>,
) {}
@Get()
async findAll() {
this.logger.log(`Hit the findAll route`);
const events = await this.repository.find();
this.logger.debug(`Found ${events.length} events`);
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(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
const event = await this.repository.findOne({
where: {
id,
},
});
if (!event) {
throw new NotFoundException();
}
return event;
}
//@UsePipes(new ValidationPipe({ groups: ['create'] }))
@Post()
async create(@Body() input: CreateEventDto) {
return await this.repository.save({
...input,
when: new Date(input.when),
});
}
@Patch(':id')
async update(@Param('id') id, @Body() input: UpdateEventDto) {
const event = await this.repository.findOne({
where: {
id,
},
});
if (!event) {
throw new NotFoundException();
}
return await this.repository.save({
...event,
...input,
when: input.when ? new Date(input.when) : event.when,
});
}
@Delete(':id')
@HttpCode(204)
async remove(@Param('id') id) {
const event = await this.repository.findOne({
where: {
id,
},
});
if (!event) {
throw new NotFoundException();
}
await this.repository.remove(event);
}
}
get localhost:3000/events/10
{
"message": "Not Found",
"statusCode": 404
}
在HTTP通信中,状态码是响应的一部分,用于表示服务器处理请求的结果。当你使用NestJS或任何其他后端框架时,浏览器或客户端发出GET请求后,服务器会返回一个响应,该响应包含状态码、响应头和响应体。
状态码200: 当请求成功,且服务器找到了请求的资源时,状态码通常是200(OK)。在这种情况下,服务器的响应体通常包含请求的资源内容,如JSON数据、HTML页面等。浏览器或客户端通常不在响应体中显示状态码,因为状态码是响应的一部分,而非响应体的内容。客户端或开发者工具(如Chrome的开发者工具)会在网络请求的概览中显示这个状态码,而不是在响应体中。
状态码404: 当服务器无法找到请求的资源时,会返回404(Not Found)状态码。在某些情况下,尤其是API开发中,开发者可能会选择在响应体中也包含状态码和错误消息,以提供更多关于错误的上下文信息。这就是为什么你可能会在响应体中看到一个statusCode字段,其中包含404状态码,以及可能的message字段来描述错误。
这种行为差异主要是由于开发者决定响应格式的方式。在成功的请求中(状态码200),通常期望的是直接获取到所请求的资源,而无需额外的状态码信息。但在错误处理的情况下(例如404错误),提供额外的错误信息(包括状态码)在响应体中对调试和错误处理是有帮助的。
在NestJS中,你可以通过异常过滤器(Exception Filters)来自定义错误响应的结构,包括决定是否在响应体中包含状态码。这提供了灵活性,允许开发者根据需要调整错误处理和成功响应的表现形式。
将状态码包含在响应体中,特别是在发生异常时,可能看起来有些重复,因为HTTP响应已经通过其状态行包含了状态码。但是,这种做法在API设计中还是有其优点和适用场景的,以下是一些理由:
增强的客户端解析能力:在响应体中包含状态码和可能的错误消息可以让客户端应用更容易地解析和处理错误。这对于那些可能不直接访问HTTP状态码的客户端尤其有用,例如某些前端框架或库。
一致性和明确性:在API响应中明确包含状态码和错误详情可以提供更一致和明确的错误处理机制。这对于维护一个大型的、由多个开发者或团队协作的API尤为重要。
自定义错误处理:在某些情况下,API可能需要返回更详细的错误信息,比如自定义错误代码或额外的错误上下文信息。将这些信息与HTTP状态码一起包含在响应体中,可以提供更丰富的错误反馈。
兼容性和灵活性:在某些复杂的系统或微服务架构中,中间件或代理可能会修改原始的HTTP状态码。在响应体中包含原始的状态码和错误信息可以确保终端用户获得准确的错误上下文。
尽管如此,最佳实践是保持API的响应尽可能清晰和简洁。如果决定在响应体中包含状态码和错误信息,应确保这样做能为API的使用者带来明确的好处。此外,应在API文档中清晰地说明这种设计选择,包括状态码的含义和可能的错误代码。
在NestJS中,你可以利用异常过滤器(Exception Filters)和拦截器(Interceptors)来灵活地处理异常和响应格式,以遵循你的API设计原则和最佳实践。例如,你可以创建一个自定义异常过滤器来格式化所有异常响应,包括状态码、错误消息和任何其他相关信息,以确保API的一致性和易用性。
一个出席者对应一个会议事件 一个会议事件对应多个出席者
出席者的 event 关联到事件的 event.attendees
@ManyToOne
默认为 event 生成列 eventId
可以手动指定列名 @JoinColumn({ name: 'event_id' })
默认将指向 event 表的 id 主键。
也可以手动指定指向 event 表的 其他 主键
referencedColumnName: 'id'
src/events/attendee.entity.ts
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
src/events/event.entity.ts
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[];
}
entities: [Event, Attendee],
src\config\orm.config.ts
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,
}),
);
src\config\orm.config.prod.ts
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,
}),
);
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);
在加载当前实体后,将硬加载其关联的实体,性能消耗更大,不推荐。
src\events\event.entity.ts
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[];
}
src\events\events.controller.ts
@Get('practice2')
async practice2() {
return await this.repository.findOne({
where: {
id: 1,
},
});
}
这将把事件的关联实体出席者attendees的全部数据返回
loadEagerRelations: false
src\events\events.controller.ts
@Get('practice2')
async practice2() {
return await this.repository.findOne({
where: {
id: 1,
},
loadEagerRelations: false,
});
}
将不加载关联的 attendees 表和字段
relations: ['attendees']
src\events\event.entity.ts
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: false })
attendees: Attendee[];
}
指定要加载的关联实体在当前表的字段
src\events\events.controller.ts
@Get('practice2')
async practice2() {
return await this.repository.findOne({
where: {
id: 1,
},
//loadEagerRelations: false,
relations: ['attendees'],
});
}
在不指定任何同步加载参数时,默认为懒加载关联实体。
src/events/events.module.ts
import { Attendee } from './attendee.entity';
@Module({
imports: [TypeOrmModule.forFeature([Event, Attendee])],
controllers: [EventsController],
})
export class EventsModule {}
需要将关联的实体存储库Repository注入到当前控制器
src/events/events.controller.ts
import { Attendee } from './attendee.entity';
constructor(
// 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
@InjectRepository(Event)
private readonly repository: Repository<Event>,
@InjectRepository(Attendee)
private readonly attendeeRepository: Repository<Attendee>,
) {}
src\events\events.controller.ts
@Get('practice2')
async practice2() {
const attendee = new Attendee();
attendee.name = 'Potter';
attendee.event = event;
await this.attendeeRepository.save(attendee);
return event;
}
src\events\events.controller.ts
@Get('practice2')
async practice2() {
const event = new Event();
event.id = 1;
const attendee = new Attendee();
attendee.name = 'Potter2';
attendee.event = event;
await this.attendeeRepository.save(attendee);
return event;
}
cascade: true
关系级联选项 cascade: true
启用后,在保存当前事件时,将同步保存关联的出席者数据。默认不会保存关联实体数据。
但这不是数据库级别的保存。
这只是 TypeOrm 在保存事件时,自动执行了保存出席者操作。
全部有效
@OneToMany(() => Attendee, (attendee) => attendee.event, { cascade: true })
attendees: Attendee[];
只对插入和更新有效
@OneToMany(() => Attendee, (attendee) => attendee.event, {
cascade: ['insert', 'update'],
})
attendees: Attendee[];
src\events\event.entity.ts
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, { cascade: true })
attendees: Attendee[];
}
获取事件关联的原出席者数据,再为其加入新的出席者,然后保存事件,这将同步保存新的出席者。
e.event = event; // 级联将导致循环引用错误 所以此行需要删除,保存时出席者attendee将自动关联eventId
src\events\events.controller.ts
@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;
}
attendees表 中 eventId 可以为空时 nullable: true, 如果事件中将事件的出席者清空 event.attendees=[],将导致attendees表中所有关联该事件的出席者的eventId全部为NULL
https://typeorm.bootcss.com/many-to-many-relations https://www.typeorm.org/many-to-many-relations
使用联合装饰器@JoinTable()
为多对对关系创建中间表
@JoinTable()
只能用在多对多的实体其中之一上。
@JoinTable()是@ManyToMany关系所必需的。 你必须把@JoinTable放在关系的一个(拥有)方面
@JoinTable({name:'demo'}) 可以定制中间表名称
src\school\school.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Subject } from './subject.entity';
import { Teacher } from './teacher.entity';
import { TrainingController } from './training.controller';
@Module({
imports: [TypeOrmModule.forFeature([Subject, Teacher])],
controllers: [TrainingController],
})
export class SchoolModule {}
拥有方启用级联
src\school\subject.entity.ts
import {
Column,
Entity,
JoinTable,
ManyToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Teacher } from './teacher.entity';
@Entity()
export class Subject {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Teacher, (teacher) => teacher.subjects, { cascade: true })
@JoinTable()
teachers: Teacher[];
}
src\school\teacher.entity.ts
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Subject } from './subject.entity';
@Entity()
export class Teacher {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Subject, (subject) => subject.teachers)
subjects: Subject[];
}
src\school\training.controller.ts
import { Controller, Post } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Subject } from './subject.entity';
import { Teacher } from './teacher.entity';
@Controller('school')
export class TrainingController {
constructor(
@InjectRepository(Subject)
private readonly subjectRepository: Repository<Subject>,
) {}
@Post('/create')
public async savingRelation() {
const subject = new Subject();
subject.name = 'Math';
const teacher1 = new Teacher();
teacher1.name = 'John Doe';
const teacher2 = new Teacher();
teacher2.name = 'Harry Doe';
subject.teachers = [teacher1, teacher2];
await this.subjectRepository.save(subject);
}
@Post('/remove')
public async removingRelation() {
const subject = await this.subjectRepository.findOne({
where: {
id: 1,
},
//loadEagerRelations: false,
relations: ['teachers'],
});
subject.teachers = subject.teachers.filter((teacher) => teacher.id !== 2);
await this.subjectRepository.save(subject);
}
}
src\app.module.ts
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';
@Module({
imports: [
// 使用 ormConfig 作为通用环境配置
ConfigModule.forRoot({
isGlobal: true,
load: [ormConfig], //通过配置服务读取配置
expandVariables: true, //启用可扩展环境变量 以支持字符串变量 ${APP_URL}
}),
// 异步加载工厂函数配置 分别用以数据库配置
TypeOrmModule.forRootAsync({
useFactory:
process.env.NODE_ENV !== 'production' ? ormConfig : ormConfigProd,
}),
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 {}
src\config\orm.config.ts
import { registerAs } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { Attendee } from 'src/events/attendee.entity';
import { Event } from './../events/event.entity';
import { Subject } from './../school/subject.entity';
import { Teacher } from './../school/teacher.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, Subject, Teacher],
synchronize: true,
}),
);
src\school\subject.entity.ts
import {
Column,
Entity,
JoinTable,
ManyToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Teacher } from './teacher.entity';
@Entity()
export class Subject {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Teacher, (teacher) => teacher.subjects, { cascade: true })
// @JoinTable 表示当前实体为拥有方
@JoinTable({
joinColumn: {
name: 'subjectId', //中间表的列名
referencedColumnName: 'id', //当前表的列名 id 关联到中间表的 subjectId
},
})
teachers: Teacher[];
}
src\school\subject.entity.ts
``
import {
Column,
Entity,
JoinTable,
ManyToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Teacher } from './teacher.entity';
@Entity()
export class Subject {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Teacher, (teacher) => teacher.subjects, { cascade: true })
// @JoinTable 表示当前实体为拥有方
@JoinTable({
joinColumn: {
name: 'subjectId', //中间表的列名
referencedColumnName: 'id', //当前表的列名 id 关联到中间表的 subjectId
},
// inverseJoinColumn 对关联的另一个老师表手动指定中间表id
inverseJoinColumn: {
name: 'teacherId', //中间表的老师id列名
referencedColumnName: 'id', //老师表的id 关联到中间表的 teacherId
},
})
teachers: Teacher[];
}
创建一个学科,2个老师
subject 表
teacher 表
中间表subject_teachers_teacher
src\events\events.controller.ts
@Get('practice2')
async practice2() {
return await this.repository
.createQueryBuilder('e')
.select(['e.id', 'e.name'])
.orderBy('e.id', 'ASC')
.take(3)
.getMany();
}
事件服务中存放业务逻辑
src/events/events.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Event } from './event.entity';
@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 async getEvent(id: number): Promise<Event | undefined> {
const query = this.getEventsBaseQuery().andWhere('e.id = :id', { id });
this.logger.debug(query.getSql()); //查询生成器 可获取实际sql语句
return await query.getOne();
}
}
src\events\events.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Attendee } from './attendee.entity';
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],
})
export class EventsModule {}
src\events\events.controller.ts
import { EventsService } from './events.service';
@Controller('/events')
export class EventsController {
private readonly logger = new Logger(EventsController.name);
constructor(
// 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
@InjectRepository(Event)
private readonly repository: Repository<Event>,
@InjectRepository(Attendee)
private readonly attendeeRepository: Repository<Attendee>,
private readonly eventsService: EventsService,
) {}
@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;
}
}
实际sql
SELECT `e`.`id` AS `e_id`, `e`.`name` AS `e_name`, `e`.`description` AS `e_description`, `e`.`when` AS `e_when`, `e`.`address` AS `e_address` FROM `event` `e` WHERE `e`.`id` = ? ORDER BY `e`.`id` DESC
src/events/events.service.ts
方法1:getCount()
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Event } from './event.entity';
@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().getCount();
}
public async getEvent(id: number): Promise<Event | undefined> {
const query = this.getEventsBaseQuery().andWhere('e.id = :id', { id });
this.logger.debug(query.getSql()); //查询生成器 可获取实际sql语句
return await query.getOne();
}
}
将 attendees 属性数量映射到事件实体的 attendeeCount 【attendeeCount 不存在于数据库中】
src/events/event.entity.ts
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, {
cascade: true,
})
attendees: Attendee[];
attendeeCount?: number;
//attendeeRejected?: number;
//attendeeMaybe?: number;
//attendeeAccepted?: number;
}
src/events/events.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Event } from './event.entity';
@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',
);
}
public async getEvent(id: number): Promise<Event | undefined> {
const query = this.getEventsWithAttendeeCountQuery().andWhere(
'e.id = :id',
{ id },
);
this.logger.debug(query.getSql()); //查询生成器 可获取实际sql语句
return await query.getOne();
}
}
src/events/attendee.entity.ts
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()
id: number;
@Column()
name: string;
@ManyToOne(() => Event, (event) => event.attendees, {
nullable: true,
})
@JoinColumn()
event: Event;
@Column('enum', {
enum: AttendeeAnswerEnum,
default: AttendeeAnswerEnum.Accepted,
})
answer: AttendeeAnswerEnum;
}
src/events/event.entity.ts
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, {
cascade: true,
})
attendees: Attendee[];
attendeeCount?: number;
attendeeRejected?: number;
attendeeMaybe?: number;
attendeeAccepted?: number;
}
src\events\events.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AttendeeAnswerEnum } from './attendee.entity';
import { Event } from './event.entity';
@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,
}),
);
}
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();
}
}
根据事件发生时间过滤数据
src\events\input\create-event.dto.ts src\events\input\update-event.dto.ts
src/events/input/list.events.ts
export class ListEvents {
when?: WhenEventFilter = WhenEventFilter.All; //枚举类型 默认为全部事件
}
export enum WhenEventFilter {
All = 1,
Today,
Tommorow,
ThisWeek,
NextWeek,
}
src\events\events.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AttendeeAnswerEnum } from './attendee.entity';
import { Event } from './event.entity';
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,
}),
);
}
public async getEventsWithAttendeeCountFiltered(filter?: ListEvents) {
let query = this.getEventsWithAttendeeCountQuery();
if (!filter) {
return query.getMany();
}
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 await query.getMany();
}
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();
}
}
异步方法getEventsWithAttendeeCountFiltered,它的作用是根据提供的过滤条件(filter)来获取事件及其参加者的数量。下面是对这个方法的详细解释:
方法结构 public async getEventsWithAttendeeCountFiltered(filter?: ListEvents):这是一个公共的异步方法,可接受一个可选的参数filter,其类型为之前定义的ListEvents类。这意味着filter可以包含一个when属性,用于指定筛选条件。 查询逻辑 初始化查询:
let query = this.getEventsWithAttendeeCountQuery();:首先,通过调用getEventsWithAttendeeCountQuery方法初始化查询。这个方法可能是在当前类中定义的,用于获取包含参加者数量的事件列表的基础查询。 无筛选条件时的快速返回:
if (!filter) { return query.getMany(); }:如果没有提供filter参数,即没有筛选条件,方法将直接执行查询并返回结果。getMany()可能是一个ORM方法,用于执行查询并获取多条记录。 应用筛选条件:
如果filter参数存在,并且其when属性有值,则根据when的值修改查询以应用相应的筛选条件。 filter.when == WhenEventFilter.Today:如果筛选条件是今天的事件,则通过andWhere方法添加SQL条件来筛选e.when字段(假设代表事件日期)在今天的事件。 filter.when == WhenEventFilter.Tommorow:如果筛选条件是明天的事件,类似地修改查询以筛选明天的事件。 filter.when == WhenEventFilter.ThisWeek:如果筛选条件是本周的事件,使用YEARWEEK函数来比较年和周,筛选本周内的事件。 filter.when == WhenEventFilter.NextWeek:如果筛选条件是下周的事件,同样使用YEARWEEK函数,并将当前周数加1来筛选下周的事件。 结果返回 最后,无论是否应用了筛选条件,都通过await query.getMany();执行最终的查询并返回结果。由于这是一个异步操作,使用await确保在返回结果前查询完成。 SQL和ORM 注意,代码中的SQL语句(如CURDATE()、INTERVAL、YEARWEEK())是针对特定SQL方言(可能是MySQL)编写的。确保这些函数在使用的数据库中是有效的。 andWhere方法看起来是用于构建查询的ORM(对象关系映射)方法,允许动态地向查询添加更多的条件。 这个方法的设计展示了如何在NestJS服务中结合使用TypeScript类和枚举,以及如何使用ORM方法来构建灵活的数据库查询。此外,通过可选参数和条件语句,这种设计还提供了高度的灵活性,使得可以根据不同的筛选条件重用同一查询逻辑。
query = query.andWhere('YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1)'); 是一个添加到SQL查询中的条件表达式,它使用YEARWEEK函数来筛选出在特定周的事件。下面是对这个表达式的详细解释:
YEARWEEK 函数 YEARWEEK(date, mode) 是一个SQL函数,常见于MySQL数据库,用于返回给定日期的年份和周数。这个函数可以接受两个参数: 第一个参数是日期值,用于计算其所属的年份和周数。 第二个参数是模式(mode),它是可选的,用于确定周的起始日和周的计算方式。在你的代码中,模式被设置为1。 参数 mode mode 参数决定了周是如何被计算的,特别是一周的起始日(通常是星期日或星期一)以及第一周是如何定义的。1 作为mode值通常意味着周是从星期一开始,且第一周是包含该年第一个星期四的那周。这是ISO周日期系统的一部分,它是许多国际标准(如ISO 8601)中使用的周计算方式。 表达式解释 YEARWEEK(e.when, 1) 计算e.when日期所在的年份和周数。这里e.when假设是数据库中某个表(例如事件表,这里用e表示)的一个字段,该字段存储了事件的日期。
YEARWEEK(CURDATE(), 1) 计算当前日期(CURDATE() 返回当前日期)所在的年份和周数。同样使用模式1,意味着采用ISO周日期系统。
YEARWEEK(e.when, 1) = YEARWEEK(CURDATE(), 1) 这个条件比较了事件日期和当前日期所在的年份和周数。如果它们在同一周内,这个条件就为真。因此,这个andWhere条件用于筛选出当前周内发生的所有事件。
使用场景 这种类型的查询在需要基于周来过滤或聚合数据时非常有用,比如在制作日历视图、报告或任何需要按周组织数据的场景中。使用YEARWEEK函数可以直接在数据库层面上完成这种复杂的日期逻辑,而不需要在应用程序代码中进行额外的日期处理,这有助于提高效率和性能。
src/events/events.controller.ts
@Get()
async findAll(@Query() filter: ListEvents) {
this.logger.debug(filter);
this.logger.log(`Hit the findAll route`);
const events =
await this.eventsService.getEventsWithAttendeeCountFiltered(filter);
this.logger.debug(`Found ${events.length} events`);
return events;
}
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Logger,
NotFoundException,
Param,
ParseIntPipe,
Patch,
Post,
Query,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Like, MoreThan, Repository } from 'typeorm';
import { Attendee } from './attendee.entity';
import { Event } from './event.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(
// 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
@InjectRepository(Event)
private readonly repository: Repository<Event>,
@InjectRepository(Attendee)
private readonly attendeeRepository: Repository<Attendee>,
private readonly eventsService: EventsService,
) {}
@Get()
async findAll(@Query() filter: ListEvents) {
this.logger.debug(filter);
this.logger.log(`Hit the findAll route`);
const events =
await this.eventsService.getEventsWithAttendeeCountFiltered(filter);
this.logger.debug(`Found ${events.length} events`);
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;
}
//@UsePipes(new ValidationPipe({ groups: ['create'] }))
@Post()
async create(@Body() input: CreateEventDto) {
return await this.repository.save({
...input,
when: new Date(input.when),
});
}
@Patch(':id')
async update(@Param('id') id, @Body() input: UpdateEventDto) {
const event = await this.repository.findOne({
where: {
id,
},
});
if (!event) {
throw new NotFoundException();
}
return await this.repository.save({
...event,
...input,
when: input.when ? new Date(input.when) : event.when,
});
}
@Delete(':id')
@HttpCode(204)
async remove(@Param('id') id) {
const event = await this.repository.findOne({
where: {
id,
},
});
if (!event) {
throw new NotFoundException();
}
await this.repository.remove(event);
}
}
src/pagination/paginator.ts
import { SelectQueryBuilder } from 'typeorm';
export interface PaginateOptions {
limit: number;
currentPage: number;
total?: boolean;
}
export interface PaginationResult<T> {
first: number;
last: number;
limit: number;
total?: number;
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 {
first: offset + 1,
last: offset + data.length,
limit: options.limit,
total: options.total ? await qb.getCount() : null,
data,
};
}
src/events/input/list.events.ts
export class ListEvents {
when?: WhenEventFilter = WhenEventFilter.All; //枚举类型 默认为全部事件
page: number = 1;
}
export enum WhenEventFilter {
All = 1,
Today,
Tommorow,
ThisWeek,
NextWeek,
}
src/events/events.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { paginate, PaginateOptions } from 'src/pagination/paginator';
import { Repository } from 'typeorm';
import { AttendeeAnswerEnum } from './attendee.entity';
import { Event } from './event.entity';
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();
}
}
使用 @UsePipes(new ValidationPipe({ transform: true })) 填充默认 filter.page
src/events/events.controller.ts
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Logger,
NotFoundException,
Param,
ParseIntPipe,
Patch,
Post,
Query,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Like, MoreThan, Repository } from 'typeorm';
import { Attendee } from './attendee.entity';
import { Event } from './event.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(
// 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
@InjectRepository(Event)
private readonly repository: Repository<Event>,
@InjectRepository(Attendee)
private readonly attendeeRepository: Repository<Attendee>,
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;
}
//@UsePipes(new ValidationPipe({ groups: ['create'] }))
@Post()
async create(@Body() input: CreateEventDto) {
return await this.repository.save({
...input,
when: new Date(input.when),
});
}
@Patch(':id')
async update(@Param('id') id, @Body() input: UpdateEventDto) {
const event = await this.repository.findOne({
where: {
id,
},
});
if (!event) {
throw new NotFoundException();
}
return await this.repository.save({
...event,
...input,
when: input.when ? new Date(input.when) : event.when,
});
}
@Delete(':id')
@HttpCode(204)
async remove(@Param('id') id) {
const event = await this.repository.findOne({
where: {
id,
},
});
if (!event) {
throw new NotFoundException();
}
await this.repository.remove(event);
}
}
数据转换:当设置为true时,transform选项会告诉ValidationPipe自动将接收到的请求数据(通常是字符串或其他基本类型)转换为声明的DTO(数据传输对象)类的实例。在这个例子中,DTO类是ListEvents。
类型转换:除了将普通的对象字面量转换为DTO类的实例之外,transform选项还能进行类型转换。例如,如果ListEvents类中有数字类型的属性,而从客户端接收到的是字符串类型的数字,这个选项会尝试将这些字符串转换为数字类型。
嵌套对象转换:当DTO中包含嵌套对象或类实例时,transform也会尝试递归地转换这些嵌套的对象。
当transform: true设置在ValidationPipe中,并且在DTO(Data Transfer Object)中存在带有默认值的属性时,如果客户端在请求中没有提供对应的参数,ValidationPipe会将这个属性设置为其在DTO中定义的默认值。
在您提供的ListEvents类中,page属性被定义并初始化为1。
使用 查询生成器 优化对大量数据和多次查询操作
src/events/events.service.ts
import { DeleteResult, Repository } from "typeorm";
@Injectable()
export class EventsService {
private readonly logger = new Logger(EventsService.name);
constructor(
@InjectRepository(Event)
private readonly eventsRepository: Repository<Event>,
) {}
public async deleteEvent(id: number): Promise<DeleteResult> {
return await this.eventsRepository
.createQueryBuilder('e')
.delete()
.where('id = :id', { id })
.execute();
}
}
src/events/events.controller.ts
@Controller('/events')
export class EventsController {
private readonly logger = new Logger(EventsController.name);
constructor(
// 通过依赖注入将实体类Event注入到当前类中作为Event存储类使用
@InjectRepository(Event)
private readonly repository: Repository<Event>,
@InjectRepository(Attendee)
private readonly attendeeRepository: Repository<Attendee>,
private readonly eventsService: EventsService,
) {}
@Delete(':id')
@HttpCode(204)
async remove(@Param('id') id) {
const result = await this.eventsService.deleteEvent(id);
if (result?.affected !== 1) {
throw new NotFoundException();
}
}
}
不应该将课程的所有属性全部加载到内存中然后再去查询,更新 应当使用查询生成器的set只更新需要更新的属性。
将所有课程名字都改为 Confidential
src\school\training.controller.ts
import { Controller, Post } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Subject } from './subject.entity';
import { Teacher } from './teacher.entity';
@Controller('school')
export class TrainingController {
constructor(
@InjectRepository(Subject)
private readonly subjectRepository: Repository<Subject>,
) {}
@Post('/remove')
public async removingRelation() {
// const subject = await this.subjectRepository.findOne({
// where: {
// id: 1,
// },
// //loadEagerRelations: false,
// relations: ['teachers'],
// });
// subject.teachers = subject.teachers.filter((teacher) => teacher.id !== 2);
// await this.subjectRepository.save(subject);
await this.subjectRepository
.createQueryBuilder('s')
.update()
.set({ name: 'Confidential' })
.execute();
}
}
查询到课程后,获取到对应的老师,再将老师全部更新,关联到课程。
src\school\training.controller.ts
import { Controller, Post } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Subject } from './subject.entity';
import { Teacher } from './teacher.entity';
@Controller('school')
export class TrainingController {
constructor(
@InjectRepository(Subject)
private readonly subjectRepository: Repository<Subject>,
@InjectRepository(Teacher)
private readonly teacherRepository: Repository<Teacher>,
) {}
@Post('/create')
public async savingRelation() {
// const subject = new Subject();
// subject.name = 'Math';
const subject = await this.subjectRepository.findOne({ where: { id: 3 } });
// const teacher1 = new Teacher();
// teacher1.name = 'John Doe';
// const teacher2 = new Teacher();
// teacher2.name = 'Harry Doe';
// subject.teachers = [teacher1, teacher2];
// await this.teacherRepository.save([teacher1, teacher2]);
const teacher1 = await this.teacherRepository.findOne({ where: { id: 5 } });
const teacher2 = await this.teacherRepository.findOne({ where: { id: 6 } });
return await this.subjectRepository
.createQueryBuilder()
.relation(Subject, 'teachers') // 关联 Subject 类 的 teachers 键
.of(subject)
.add([teacher1, teacher2]);
}
@Post('/remove')
public async removingRelation() {
// const subject = await this.subjectRepository.findOne({
// where: {
// id: 1,
// },
// //loadEagerRelations: false,
// relations: ['teachers'],
// });
// subject.teachers = subject.teachers.filter((teacher) => teacher.id !== 2);
// await this.subjectRepository.save(subject);
await this.subjectRepository
.createQueryBuilder('s')
.update()
.set({ name: 'Confidential' })
.execute();
}
}
代码段中的savingRelation方法是TrainingController类的一部分,这个类被装饰为一个NestJS控制器,负责处理与school路由相关的请求。savingRelation方法特别是用来处理与/school/create路由相关的POST请求。其主要功能是在Subject实体和Teacher实体之间建立关联。下面是对这个方法的详细解析:
方法概览 首先,方法通过this.subjectRepository.findOne({ where: { id: 3 } })获取ID为3的Subject实体实例。这里假设Subject实体代表了学科,例如数学、科学等。 接着,方法通过两次调用this.teacherRepository.findOne(),分别获取ID为5和6的Teacher实体实例。这里Teacher实体代表教师。 最后,方法使用TypeORM的createQueryBuilder()和relation()方法,将找到的两个教师实体添加到之前找到的学科实体的teachers关联中。这表明这两个教师教授这个学科。 关键代码解析 TypeORM存储库注入:@InjectRepository(Subject)和@InjectRepository(Teacher)装饰器用于注入对应的TypeORM存储库。这允许你在控制器中使用存储库API来执行数据库操作。
查找实体:this.subjectRepository.findOne({ where: { id: 3 } })和this.teacherRepository.findOne({ where: { id: x } })分别用于查找特定ID的学科和教师实体。这些操作通常返回一个Promise,需要使用await来解析结果。
建立关联:
this.subjectRepository.createQueryBuilder()初始化一个QueryBuilder,用于构建和执行SQL查询。 .relation(Subject, 'teachers')指定我们要操作的关联。这里'teachers'应该是Subject实体中定义的一个关联属性,表示一个学科可以有多个教师。 .of(subject)指定了我们要添加关联的目标实体,即之前找到的特定学科实体。 .add([teacher1, teacher2])将两个教师实体添加到学科的teachers关联中。这意味着在数据库中,会建立这两个教师与该学科之间的关系。 方法的作用 这个方法展示了如何在NestJS应用程序中使用TypeORM来操作实体间的关系。具体到这个例子中,它演示了如何将教师与学科关联起来,这在实现诸如学校管理系统这样的应用时非常常见。通过这种方式,可以灵活地添加或修改实体间的关联,而无需直接操作关联表。
清空老师,课程中间表,使用数据库中存在的相关老师,课程id测试。
post localhost:3000/school/create 将生成关联老师,课程的中间表数据
优化了关联数据的操作。
用户和用户档案时一对一关系
src/auth/profile.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column()
age: number;
}
src/auth/user.entity.ts
import {
Column,
Entity,
JoinColumn,
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Profile } from './profile.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
@Column()
password: string;
@Column()
email: string;
@Column()
firstName: string;
@Column()
lastName: string;
@OneToOne(() => Profile)
@JoinColumn()
profile: Profile;
}
src/config/orm.config.ts
import { registerAs } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { Profile } from 'src/auth/profile.entity';
import { User } from 'src/auth/user.entity';
import { Attendee } from 'src/events/attendee.entity';
import { Event } from './../events/event.entity';
import { Subject } from './../school/subject.entity';
import { Teacher } from './../school/teacher.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, Subject, Teacher, User, Profile],
synchronize: true,
}),
);
// const user = new User();
// const profile = new Profile();
// user.profile = profile;
// user.profile = null;
// Save the user here
使用用户名和密码进行验证
npm i -S @nestjs/passport passport passport-local
npm i -D @types/passport-local
src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
@Module({
imports: [],
providers: [],
})
export class AuthModule {}
src/app.module.ts
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 {}
src/auth/local.strategy.ts
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;
}
}
src/auth/auth.module.ts
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 {}
src/auth/auth.controller.ts
import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller('auth')
export class AuthController {
}
src/auth/auth.module.ts
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 {}
src/auth/auth.controller.ts
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 来验证 post localhost:3000/auth/login
令牌 不要包含密码等敏感数据。
令牌不需要数据库查询,且负载应尽量小。
npm i -S @nestjs/jwt passport-jwt
npm i -D @types/passport-jwt
src/auth/auth.module.ts
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
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
src/auth/auth.service.ts
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模块
src/auth/auth.module.ts
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 {}
src/auth/auth.controller.ts
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),
};
}
}
src/auth/jwt.strategy.ts
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中供后续路由使用
}
}
src/auth/auth.controller.ts
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;
}
}
src/auth/auth.module.ts
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 {}
添加请求头 Authorization
值为 Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRvbSIsInN1YiI6MSwiaWF0IjoxNzA4NjE4NjE0LCJleHAiOjE3MDg2MjIyMTR9.cRhlY2yPuUN2ShrMqGvp9x8D8R6taiXO8p4JImtpgKE
npm i -S bcrypt
npm i -D @types/bcrypt
src/auth/auth.service.ts
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);
}
}
src/auth/local.strategy.ts
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;
}
}
使用自定义参数对用户进行验证
src/auth/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user ?? null;
},
);
src/auth/auth.controller.ts
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;
}
}
src/auth/user.entity.ts
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;
}
src/auth/input/create.user.dto.ts
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;
}
src/auth/users.controller.ts
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),
};
}
}
src/auth/auth.module.ts
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
一个组织者对应多个事件
src/events/event.entity.ts
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;
}
src/auth/user.entity.ts
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[];
}
src/events/events.service.ts
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();
}
}
src/events/events.controller.ts
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);
}
src/auth/auth-guard.jwt.ts
import { AuthGuard } from '@nestjs/passport';
export class AuthGuardJwt extends AuthGuard('jwt') {}
src/auth/auth-guard.local.ts
import { AuthGuard } from '@nestjs/passport';
export class AuthGuardLocal extends AuthGuard('local') {}
src/auth/auth.controller.ts
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;
}
}
src/events/events.controller.ts
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);
}
src/events/events.service.ts
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();
}
}
src/events/events.controller.ts
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);
}
}
拦截器可以转换数据,转换异常,增加缓存功能。可以修改控制器返回的数据。
序列化转换实体的json数据,只响应需要的数据给用户。
序列化 有两种策略,排除所有实体参数或公开所有实体参数
src/events/events.controller.ts
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 {
@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;
}
公开 的参数才会响应给用户
src/events/event.entity.ts
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;
}
src/events/attendee.entity.ts
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;
}
src/auth/user.entity.ts
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[];
}
src/auth/auth.controller.ts
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;
}
}
src/events/events.controller.ts
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依赖类的元数据来应用序列化规则。如果返回的数据不是通过类构造函数创建的实例,而是简单的对象字面量,那么可能不会应用任何序列化规则。 所以 findAll 将不会返回数据。
序列化时,需要通过实体的 @Expose() 参数决定是否返回 如果序列化的是一个普通对象,将返回空。 序列化的目标必须是实体的类的实例。
src/pagination/paginator.ts
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,
});
}
现在 findAll 可以返回数据
由于开发阶段在不断个更新列名,旧数据的新列名将为空,发生错误,需要清空数据表 truncate-all-tables.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语句用于清空多个表的内容,并在此过程中暂时禁用外键约束检查,以避免因外键依赖而导致的删除错误。下面是对这个SQL语句的逐步解析:
SET foreign_key_checks = 0;
foreign_key_checks
设置为0
,禁用了外键约束检查。这在你需要批量删除表或清空包含相互依赖外键的表时非常有用,因为它避免了因外键约束而导致的删除失败。接下来的几条TRUNCATE TABLE
语句用于清空指定的表:
TRUNCATE TABLE `attendee`;
TRUNCATE TABLE `event`;
TRUNCATE TABLE `profile`;
TRUNCATE TABLE `subject`;
TRUNCATE TABLE `subject_teachers_teacher`;
TRUNCATE TABLE `teacher`;
TRUNCATE TABLE `user`;
TRUNCATE TABLE
语句用于删除表中的所有行,并重置表的自增计数器(如果有的话)。与使用DELETE FROM
语句删除所有行相比,TRUNCATE TABLE
通常更快,且不记录每行的删除动作,这意味着它不会占用大量的事务日志空间。attendee
、event
、profile
、subject
、subject_teachers_teacher
、teacher
和user
。这些表名暗示了一个涉及到事件、参加者、用户档案、教师和教学科目的应用程序数据库模型。SET foreign_key_checks = 1;
foreign_key_checks
设置为1
来重新启用外键约束检查。这确保了在之后的数据库操作中,所有的外键约束都将被正常检查,以维护数据完整性。这样的操作通常用于开发或测试环境,在需要快速清空数据库内容并从头开始时非常有用。但是,在生产环境中直接使用这种方法需要非常谨慎,因为它会永久删除所有指定表中的数据,并且不能恢复。
总的来说,这段SQL语句是一种快速清理数据库内容的方法,通过暂时禁用外键约束来避免在清空表时遇到依赖问题。在执行这类操作时,建议确保已经做好了适当的数据备份,以防不测。
src/auth/user.entity.ts
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[];
}
src/events/attendee.entity.ts
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;
}
src/events/event.entity.ts
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>;
控制器只应该获取用户输入,然后响应。 业务逻辑应放在服务中。
获取所有参与指定事件的出席者
这里Attendee实体中有一个名为event的关联属性,该属性链接到一个事件实体,而这个事件实体有一个id字段。
src/events/attendees.service.ts
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 } },
});
}
}
src/events/event-attendees.controller.ts
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);
}
}
src/events/events.service.ts
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,
});
}
}
src/events/events-organized-by-user.controller.ts
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 },
);
}
}
src/events/events.module.ts
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 {}
src/events/input/create-attendee.dto.ts
import { IsEnum } from 'class-validator';
import { AttendeeAnswerEnum } from './../attendee.entity';
export class CreateAttendeeDto {
@IsEnum(AttendeeAnswerEnum)
answer: AttendeeAnswerEnum;
}
src/events/attendees.service.ts
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);
}
}
src/events/events.service.ts
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 });
}
}
src/events/current-user-event-attendance.controller.ts
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);
}
}
用户和出席者不能全部公开,否则会导致循环引用。
只公开出席者中的user. 不公开用户中的attended
src/events/current-user-event-attendance.controller.ts
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);
}
}
src/events/event.entity.ts
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>;
src/events/events-organized-by-user.controller.ts
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 },
);
}
}
src/events/events.service.ts
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 });
}
}
src/events/events.controller.ts
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);
}
}
src/events/events.module.ts
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 {}
精通 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