nestjs结合graphql开发入门

2,228 阅读5分钟

一、环境配置

  • 1、官网地址

    官网提供了两种方式来操作graphql

    • 传统的方式先定义schema
    • 直接使用typescript-graphql的对象方式来创建
  • 2、安装依赖包

    npm install @nestjs/graphql graphql-tools graphql apollo-server-express
    
  • 3、在app.module.ts文件中添加

    import { Module } from '@nestjs/common';
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    import { GraphQLModule } from '@nestjs/graphql';
    
    @Module({
      imports: [
        GraphQLModule.forRoot({
          // 自动生成schema.graphql文件
          autoSchemaFile: 'schema.graph',
        })
      ],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule { }
    
  • 4、删除脚手架创建的AppController改为resolver

    graphqlresolver叫解析器,主要包括query(查询数据)、mutation(增、删、改数据)、subscription(订阅,有点类型socket),在graphql项目中我们用resolver替换了之前的控制器

    nest g r app
    
    ➜  src git:(master) ✗ tree          
    .
    ├── app.module.ts
    ├── app.resolver.ts
    ├── app.service.ts
    └── main.ts
    

二、第一个graphql程序

  • 1、定义resolver

    import { Resolver, Query } from '@nestjs/graphql';
    
    @Resolver()
    export class AppResolver {
      @Query(() => String) // 定义一个查询,并且返回字符类型
      hello() {
        return 'hello world';
      }
    }
    
  • 2、直接启动程序会报错

  • 3、修复上面这个错误,直接在tsconfig.json文件中添加一行就可以

    "compilerOptions": {
      "esModuleInterop": true,
    }
    
  • 4、查看项目下是否自动生成一个schema.graph文件

  • 5、启动程序并且在浏览器上访问http://localhost:3000/graphql

二、在nestjs中集成typeorm

  • 1、目前在nestjs中使用graphql api对接数据库常见的orm有:

    本人一直使用的是typeorm就先不去折腾prisma来对接数据库

  • 2、安装typeorm对接mysql的包

    npm install --save @nestjs/typeorm typeorm mysql
    
  • 3、在app.module.ts中配置数据库连接信息

    import { Module } from '@nestjs/common';
    import { AppService } from './app.service';
    import { GraphQLModule } from '@nestjs/graphql';
    import { AppResolver } from './app.resolver';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { UserModule } from './user/user.module';
    
    @Module({
      imports: [
        TypeOrmModule.forRoot({
          type: 'mysql',
          host: 'localhost',
          port: 3306,
          username: 'root',
          password: '123456',
          database: 'typeorm_mysql',
          entities: [__dirname + '/**/*.entity{.ts,.js}'],
          logging: true,
          synchronize: true,
          extra: {
            poolMax: 32,
            poolMin: 16,
            queueTimeout: 60000,
            pollPingInterval: 60, // 每隔60秒连接
            pollTimeout: 60, // 连接有效60秒
          }
        }),
        GraphQLModule.forRoot({
          // 自动生成schema.graphql文件
          autoSchemaFile: 'schema.graph',
        }),
        UserModule, // 创建的用户模块
      ],
      controllers: [],
      providers: [AppService, AppResolver],
    })
    export class AppModule { }
    

三、将用户模块改造成graphql查询方式

  • 1、将实体类改造成可以被识别的graphql

    import { Entity, BaseEntity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
    import { ObjectType, Field } from '@nestjs/graphql';
    
    @ObjectType() // 在类上加上这个装饰器
    @Entity('user')
    export class UserEntity extends BaseEntity {
      @Field() // 需要返回的字段加上这个
      @PrimaryGeneratedColumn({
        type: 'int',
        name: 'id',
        comment: '主键id'
      })
      id: number;
    
      @Field()
      @Column({
        type: 'varchar',
        unique: true,
        length: 50,
        nullable: false,
        name: 'username',
      })
      username: string;
    
      // 这个字段不返回就不加的
      @Column({
        type: 'varchar',
        length: 100,
        nullable: false,
        name: 'password',
      })
      password: string;
    
      @Field()
      @CreateDateColumn({
        type: 'timestamp',
        nullable: false,
        name: 'created_at',
        comment: '创建时间'
      })
      createdAt: Date;
    
      @Field()
      @UpdateDateColumn({
        type: 'timestamp',
        nullable: false,
        name: 'updated_at',
        comment: '更新时间',
      })
      updatedAt: Date;
    }
    
  • 2、在user.resolver.ts中定义接收前端参数,将数据发送到服务层存储到数据库中

    import { Resolver, Mutation, Args } from '@nestjs/graphql';
    import { UserService } from './user.service';
    import { UserEntity } from './user.entity';
    
    @Resolver()
    export class UserResolver {
      constructor (
        private readonly userService: UserService,
      ) { }
    
      // 第一个参数的返回值,name是定义别名,如果不定义就会默认以方法名
      @Mutation(() => UserEntity, { nullable: true, name: 'register' })
      async register(
        @Args('username') username: string, // 使用@Args接收客户端参数
        @Args('password') password: string,
      ): Promise<UserEntity> {
        return await this.userService.register(username, password);
      }
    }
    
  • 3、服务层使用typeorm将数据存储到数据库中

    import { Injectable } from '@nestjs/common';
    import { UserEntity } from './user.entity';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Repository } from 'typeorm';
    
    @Injectable()
    export class UserService {
      constructor (
        @InjectRepository(UserEntity)
        private readonly userRepository: Repository<UserEntity>,
      ) { }
    
      /**
       * 用户注册
       * @param username 
       * @param password 
       */
      async register(username: string, password: string): Promise<UserEntity> {
        const user = this.userRepository.create({ username, password });
        return await this.userRepository.save(user);
      }
    }
    
  • 4、客户端将数据存储到数据库中

  • 5、定义一个查询全部数据

    import { Resolver, Mutation, Args, Query } from '@nestjs/graphql';
    import { UserService } from './user.service';
    import { UserEntity } from './user.entity';
    
    @Resolver()
    export class UserResolver {
      constructor (
        private readonly userService: UserService,
      ) { }
    
      @Query(() => [UserEntity!]!, { nullable: true })
      async userList(): Promise<UserEntity[]> {
        return await this.userService.userList();
      }
    }
    

  • 6、查询一条数据

    import { Resolver, Mutation, Args, Query } from '@nestjs/graphql';
    import { UserService } from './user.service';
    import { UserEntity } from './user.entity';
    
    @Resolver()
    export class UserResolver {
      constructor (
        private readonly userService: UserService,
      ) { }
    
      @Query(() => UserEntity, { nullable: true, name: 'user' })
      async userById(
        @Args('id') id: number
      ): Promise<UserEntity> {
        return await this.userService.userById(id);
      }
    }
    
    

  • 7、删除数据

    import { Resolver, Mutation, Args, Query } from '@nestjs/graphql';
    import { UserService } from './user.service';
    import { UserEntity } from './user.entity';
    
    @Resolver()
    export class UserResolver {
      constructor (
        private readonly userService: UserService,
      ) { }
    
      ....
    
      @Mutation(() => String, { nullable: true })
      async deleteById(
        @Args('id') id: number
      ): Promise<string> {
        return await this.userService.deleteById(id);
      }
    }
    
    

  • 8、修改数据

    import { Resolver, Mutation, Args, Query } from '@nestjs/graphql';
    import { UserService } from './user.service';
    import { UserEntity } from './user.entity';
    
    @Resolver()
    export class UserResolver {
      constructor (
        private readonly userService: UserService,
      ) { }
    
      ...
      @Mutation(() => String, { nullable: true, name: 'modifyById' })
      async modifyById(
        @Args('id') id: number,
        @Args('username') username: string,
        @Args('password') password: string,
      ): Promise<string> {
        return await this.userService.modifyById(id, username, password);
      }
    }
    

四、使用dto对入参数据校验

  • 1、安装包

    npm install --save class-validator class-transformer
    
  • 2、在main.ts中使用全局管道

    app.useGlobalPipes(new ValidationPipe());
    
  • 3、定义用户注册的dto

    import { InputType, Field } from '@nestjs/graphql';
    import { IsNotEmpty, MinLength } from 'class-validator';
    
    @InputType()
    export class RegisterDto {
      @Field()
      @MinLength(3, { message: '最小长度为3' })
      @IsNotEmpty({ message: '用户名不能为空' })
      username: string;
    
      @Field()
      @IsNotEmpty({ message: '密码不能为空' })
      password: string;
    }
    
  • 4、在resolver中使用

    @Mutation(() => UserEntity, { nullable: true, name: 'register' })
    async register(
      @Args('data') registerDto: RegisterDto, // data是随便定义的
    ): Promise<UserEntity> {
      return await this.userService.register(registerDto);
    }
    
  • 5、调用方式

五、使用jwt做登录返回token

  • 1、安装jsonwebtoken的包

    npm install jsonwebtoken
    
  • 2、在用户的实体类中定义一个生成token的字段

    @Field(() => String)
    get token() {
      // 第一个参数是将什么加密进去,第二个参数是加盐,这里是演示就随便写一个,第三个是其它参数,这设置过期时间为3d
      return jwt.sign({ id: this.id, username: this.username }, 'abc', {
        expiresIn: '3d'
      })
    }
    
  • 3、登录接口

    @Mutation(() => UserEntity, { nullable: true })
    async login(
      @Args('data') loginDto: LoginDto,
    ): Promise<UserEntity> {
      return await this.userService.login(loginDto);
    }
    
  • 4、测试接口

六、使用守卫来守卫别的接口

  • 1、安装依赖包参考文档

    npm install passport @nestjs/passport @nestjs/jwt passport-jwt
    npm install @types/passport-jwt -D
    
  • 2、创建一个守卫的模块

    ➜  auth git:(master) ✗ tree .             
    .
    ├── auth.module.ts
    ├── gql-auth.guard.ts
    └── jwt.strategy.ts
    
    0 directories, 3 files
    
  • 3、auth.module.ts模块的代码

    import { Module } from '@nestjs/common';
    import { JwtModule } from '@nestjs/jwt';
    import { JwtStrategy } from './jwt.strategy';
    
    @Module({
      imports: [
        JwtModule.register({
          secret: 'abc', // 这个地方要和加密的一样,在实际项目中会单独定义变量
        })
      ],
      providers: [JwtStrategy]
    })
    export class AuthModule { }
    
  • 4、jwt.strategy.ts的代码

    关于实现原理可以参考这个源码,如果对这种方式实现不满意,可以自己定义来实现token的验证

    import { Injectable, UnauthorizedException } from '@nestjs/common';
    import { PassportStrategy } from '@nestjs/passport';
    import { Strategy, ExtractJwt } from 'passport-jwt';
    import { UserEntity } from 'src/user/user.entity';
    
    @Injectable()
    export class JwtStrategy extends PassportStrategy(Strategy) {
      constructor () {
        super({
          jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
          ignoreExpiration: false,
          secretOrKey: 'abc',
        });
      }
    
      async validate(payload: { id: number, username: string }) {
        // 从token中解析出id,去查找当前用户,然后挂到req上
        const user = await UserEntity.findOne(payload.id);
        if (!user) {
          throw new UnauthorizedException('No such user');
        }
        return user;
      }
    }
    
  • 5、关于gql-auth.guard.ts的内容

    import { Injectable, ExecutionContext } from '@nestjs/common';
    import { AuthGuard } from '@nestjs/passport';
    import { GqlExecutionContext } from '@nestjs/graphql';
    
    @Injectable()
    export class GqlAuthGuard extends AuthGuard('jwt') {
      getRequest(context: ExecutionContext) {
        const ctx = GqlExecutionContext.create(context);
        return ctx.getContext().req;
      }
    }
    
  • 6、修改app.module.ts文件

    import { Module } from '@nestjs/common';
    import { AppService } from './app.service';
    import { GraphQLModule } from '@nestjs/graphql';
    import { AppResolver } from './app.resolver';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { UserModule } from './user/user.module';
    import { AuthModule } from './auth/auth.module';
    
    @Module({
      imports: [
        ...
        GraphQLModule.forRoot({
          // 自动生成schema.graphql文件
          autoSchemaFile: 'schema.graph',
          context: ({ req }) => ({ req }), // 新增这行,将数据挂载到context上
        }),
        UserModule,
        AuthModule
      ],
      controllers: [],
      providers: [AppService, AppResolver],
    })
    export class AppModule { }
    
  • 7、在resolver中使用守卫

    @UseGuards(GqlAuthGuard)
    @Query(() => UserEntity, { nullable: true, name: 'user' })
    async userById(
      @Args('id') id: number,
      @Context() context: any,
      ): Promise<UserEntity> {
        console.log(context.req ?.user, '请求头的数据')
        return await this.userService.userById(id);
    }
    
  • 8、测试数据

    {
      "Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbjEiLCJpYXQiOjE2MDk2NjQ1NzcsImV4cCI6MTYwOTkyMzc3N30.H64VYR6plKDhki4wGBSSSsrRDaL51tQY55lZuhQ743U"
    }