jiayisheji / blog

没事写写文章,喜欢的话请点star,想订阅点watch,千万别fork!
https://jiayisheji.github.io/blog/
505 stars 49 forks source link

在NestJS中使用Typegoose分享 #30

Open jiayisheji opened 4 years ago

jiayisheji commented 4 years ago

今天,我将与你分享在使用NestJS和MongoDB时一直在使用的工作流程/技术。 此工作流程利用了Typegoose的功能。

背景:最近在升级 nest-cnode 项目,之前使用的是 Mongoose,它的操作和Typescript有点冲突,创建schemainterface要写2个基本一样的,这样就比较累,虽然可以用工具生成,但是还是多了一个步骤,有没有更简单的呢,一开始想到 Typeorm ,看样子很不错的。

ORM for TypeScript and JavaScript (ES7, ES6, ES5). Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, SAP Hana, WebSQL databases. Works in NodeJS, Browser, Ionic, Cordova and Electron platforms.

Typeorm的简介,虽说支持很多数据库,MongoDB也是支持的,还有一篇专门介绍兼容MongoDB的文档,我在另外一个项目使用了一下,发现支持的并不是那么好,这也难怪没有写 Supports。需要寻找一个替代品,谷歌搜索找到了 Typegoose

注意:Typegoose是有2个版本,这点你可以github搜索,szokodiakos/typegoosetypegoose/typegoose

关于这2个版本有什么不同呢?

基本差别不大,szokodiakos/typegoose的暂停维护,查看原因typegoose/typegoose相当一个分支,不过两者写法还是有点不同。

szokodiakos/typegoose

class User extends Typegoose {
  @prop()
  name?: string;
}
const UserModel = new User().getModelForClass(User);
// UserModel is a regular Mongoose Model with correct types
(async () => {
  const u = await UserModel.create({ name: 'JohnDoe' });
  const user = await UserModel.findOne();
  console.log(user); 
// prints { _id: 59218f686409d670a97e53e0, name: 'JohnDoe', __v: 0 }
})();

typegoose/typegoose

class User {
  @prop()
  public name?: string;
}
const UserModel = getModelForClass(User);
// UserModel is a regular Mongoose Model with correct types
(async () => {
  const { _id: id } = await UserModel.create({ name: 'JohnDoe' } as User); // an "as" assertion, to have types for all properties
  const user = await UserModel.findById(id).exec();

  console.log(user); 
// prints { _id: 59218f686409d670a97e53e0, name: 'JohnDoe', __v: 0 }
})();

两者差别,一个是类继承,一个是函数。

两个目的都是一样,正如它们文档介绍那样:Typegoose - Define Mongoose models using TypeScript classes.

它们出现也是正如前面的困惑一样。介绍这么多,该进入正题了。

学习Typegoose,你需要对Mongoose熟悉,Typegoose就是在操作Mongoose。只是帮我们整合到TypeScript classes里

让我们开始吧。

首先使用@nestjs/cli初始化一个新的NestJS应用程序

nest new nest-typegoose-demo
cd nest-typegoose

接下来,让我们安装依赖项:

npm install --save @nestjs/mongoose mongoose @typegoose/typegoose
npm install --save-dev @types/mongoose

vs code 打开项目

然后,删除 app.controller.tsapp.service.ts。修改你的app.module.ts

使用@nestjs/mongoose连接我们的Mongo连接。

app.module.ts

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    MongooseModule.forRoot('mongodb://localhost:27017/nestjs-typegoose-demo', {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      useCreateIndex: true,
    }),
  ],
})
export class AppModule {}

运行你的MongoDB

现在,你可以尝试运行服务器:

npm run start:dev

你应该会看到以下内容:

21:42:04 - File change detected. Starting incremental compilation...

21:42:04 - Found 0 errors. Watching for file changes.
[Nest] 14056   - 2020-03-16 21:42:09   [NestFactory] Starting Nest application...
[Nest] 14056   - 2020-03-16 21:42:09   [InstanceLoader] AppModule dependencies initialized +23ms
[Nest] 14056   - 2020-03-16 21:42:09   [InstanceLoader] MongooseModule dependencies initialized +0ms
[Nest] 14056   - 2020-03-16 21:42:09   [InstanceLoader] MongooseCoreModule dependencies initialized +15ms
[Nest] 14056   - 2020-03-16 21:42:09   [NestApplication] Nest application successfully started +7ms

我习惯把数据库管理单独放在一起,这样便于管理,也利于分层,在写法上,不会出现循环依赖的问题。虽然nest有解决方法,但是能避免就应该避免一下。

mkdir src/models // 存放我们所有的数据库管理
touch src/models/base.model.ts // 基础模型文件 存放通用模型,作为抽象父类
touch src/models/base.repository.ts // 基本服务文件 等下会说明

打开 base.model.ts 然后输入以下内容

import { Schema } from 'mongoose';
import { buildSchema, prop } from '@typegoose/typegoose';
import { AnyType } from 'src/shared/interfaces';
export abstract class BaseModel {
    @prop()
    created_at?: Date; // 创建时间
    @prop()
    updated_at?: Date; // 更新时间

    public id?: string; // 实际上是 model._id getter

    // 如果需要,可以向基本模型添加更多内容。

    static get schema(): Schema {
        return buildSchema(this as AnyType, {
            timestamps: {
                createdAt: 'created_at',
                updatedAt: 'updated_at',
            },
            toJSON: {
                virtuals: true,
                getters: true,
                versionKey: false,
            },
        });
    }

    static get modelName(): string {
        return this.name;
    }
}

先来理解一下上面的代码:

  1. 创建一个抽象通用基础类,遵守Typescript规范,抽象类不能直接new,必须继承才能使用。
  2. created_at,`updated_at和id是我所有领域模型的三个字段(你还可以添加更多通用属性或Schema字段)。timestamps启用映射我们创建时间和更新时间自动更新。id实际上是_id的一个getter,所以它总是在那里,但是为了能够在与.lean()或.toJSON()匹配时获取id,我们需要设置toJSON:{…}选项,如代码所示
  3. @prop() 将字段注释为Schema的一部分。在typegoose了解更多
  4. static get schema() 神奇的地方就在这里,typegoose暴露诸如getModelForClass()buildSchema()之类的函数,我们只需要创建纯就可以通过这些方法和typegoose做绑定关联。为什么需要buildSchema(),那是因为我们使用@nestjs/mongoose,通过MongooseModule.forFeature注册mongoose.model,需要2个必须属性:nameSchema。 这里buildSchema()就是生成Schema的方法。它的工作原理是我们调用buildSchema()并传入this。在这种情况下,在静态方法中,实际的类本身调用schema getter,这使得将get schema()放在抽象类上成为可能,这样我们就可以在这里通用处理一下。在此之前,我们需要编写方法来获取每个领域模型类的模式和模型名,这有点像样板文件。
  5. static get modelName() 就很简单。我们只返回this.name,并且这个,同样在静态方法的上下文中,指向实际的类,所以this.name返回类名。然而,如果你持怀疑态度,你可能想要返回一些其他的东西,或者只是有一个函数,它会为你的MongooseModel返回一些有意义的名字。我倾向于在这里返回this.name,因为我在更多的地方使用this.name,比如swag UI来标注tags

现在我们有了基础模型,让我们来处理基础服务。打开base.repository.ts并粘贴以下代码。但是在显示代码之前,我想解释一下。

什么是BaseRepository ?BaseRepository是Repository模式。然而,使用像mongoose这样的ODM,我感觉MongooseModel已经有点像一个存储库了。完全可以去掉存储库层,以减少整个应用程序中抽象的数量。同样,这取决于应用程序需求的特征。我只是想让大家明白我的观点,并解释我为什么要这么做。还有一个好处减少循环依赖。现在我们已经清楚了,让我们继续:

import {
    ModelPopulateOptions,
    QueryFindOneAndUpdateOptions,
    Types,
    DocumentQuery,
    QueryFindOneAndRemoveOptions,
    Query,
} from 'mongoose';
import { WriteOpResult, FindAndModifyWriteOpResultObject, MongoError } from 'mongodb';
import { Transform } from 'class-transformer';
import { IsOptional, Max, Min } from 'class-validator';

import { AnyType } from 'src/shared/interfaces';
import { BaseModel } from './base.model';
import { ReturnModelType, DocumentType } from '@typegoose/typegoose';
import { AnyParamConstructor } from '@typegoose/typegoose/lib/types';
import { InternalServerErrorException } from '@nestjs/common';

export type OrderType<T> = Record<keyof T, 'asc' | 'desc' | 'ascending' | 'descending' | 1 | -1>;

export type QueryList<T extends BaseModel> = DocumentQuery<Array<DocumentType<T>>, DocumentType<T>>;
export type QueryItem<T extends BaseModel> = DocumentQuery<DocumentType<T>, DocumentType<T>>;

/**
 * Describes generic pagination params
 */
export abstract class PaginationParams<T> {
    /**
     * Pagination limit
     */
    @IsOptional()
    @Min(1)
    @Max(50)
    @Transform((val: string) => parseInt(val, 10) || 10)
    public readonly limit = 10;

    /**
     * Pagination offset
     */
    @IsOptional()
    @Min(0)
    @Transform((val: string) => parseInt(val, 10))
    public readonly offset: number;

    /**
     * Pagination page
     */
    @IsOptional()
    @Min(1)
    @Transform((val: string) => parseInt(val, 10))
    public readonly page: number;

    /**
     * OrderBy
     */
    @IsOptional()
    public abstract readonly order?: OrderType<T>;
}

/**
 * 分页器返回结果
 * @export
 * @interface Paginator
 * @template T
 */
export interface Paginator<T> {
    /**
     * 分页数据
     */
    items: T[];
    /**
     * 总条数
     */
    total: number;
    /**
     * 一页多少条
     */
    limit: number;
    /**
     * 偏移
     */
    offset?: number;
    /**
     * 当前页
     */
    page?: number;
    /**
     * 总页数
     */
    pages?: number;
}

export abstract class BaseRepository<T extends BaseModel> {
    constructor(protected model: ReturnModelType<AnyParamConstructor<T>>) { }

    /**
     * @description 抛出mongodb异常
     * @protected
     * @static
     * @param {MongoError} err
     * @memberof BaseRepository
     */
    protected static throwMongoError(err: MongoError): void {
        throw new InternalServerErrorException(err, err.errmsg);
    }

    /**
     * @description 将字符串转换成ObjectId
     * @protected
     * @static
     * @param {string} id
     * @returns {Types.ObjectId}
     * @memberof BaseRepository
     */
    protected static toObjectId(id: string): Types.ObjectId {
        try {
            return Types.ObjectId(id);
        } catch (e) {
            this.throwMongoError(e);
        }
    }

    /**
     * @description 创建模型
     * @param {Partial<T>} [doc]
     * @returns {DocumentType<T>}
     * @memberof BaseRepository
     */
    createModel(doc?: Partial<T>): DocumentType<T> {
        return new this.model(doc);
    }

    /**
     * @description 获取指定条件全部数据
     * @param {*} conditions
     * @param {(Object | string)} [projection]
     * @param {({
     *     sort?: OrderType<T>;
     *     limit?: number;
     *     skip?: number;
     *     lean?: boolean;
     *     populates?: ModelPopulateOptions[] | ModelPopulateOptions;
     *     [key: string]: any;
     *   })} [options]
     * @returns {QueryList<T>}
     */
    public findAll(conditions: AnyType, projection?: object | string, options: {
        sort?: OrderType<T>;
        limit?: number;
        skip?: number;
        lean?: boolean;
        populates?: ModelPopulateOptions[] | ModelPopulateOptions;
        [key: string]: AnyType;
    } = {}): QueryList<T> {
        return this.model.find(conditions, projection, options);
    }

    public async findAllAsync(conditions: AnyType, projection?: object | string, options: {
        sort?: OrderType<T>;
        limit?: number;
        skip?: number;
        lean?: boolean;
        populates?: ModelPopulateOptions[] | ModelPopulateOptions;
        [key: string]: AnyType;
    } = {}): Promise<Array<DocumentType<T>>> {
        const { populates = null, ...option } = options;
        const docsQuery = this.findAll(conditions, projection, option);
        try {
            return await this.populates<Array<DocumentType<T>>>(docsQuery, populates);
        } catch (e) {
            BaseRepository.throwMongoError(e);
        }
    }

    /**
     * @description 获取带分页数据
     * @param {PaginationParams<T>} conditions
     * @param {(Object | string)} [projection]
     * @param {({
     *     lean?: boolean;
     *     populates?: ModelPopulateOptions[] | ModelPopulateOptions;
     *     [key: string]: any;
     *   })} [options={}]
     * @returns {Promise<Paginator<T>>}
     */
    public async paginator(conditions: PaginationParams<T>, projection?: object | string, options: {
        lean?: boolean;
        populates?: ModelPopulateOptions[] | ModelPopulateOptions;
        [key: string]: AnyType;
    } = {}): Promise<Paginator<T>> {
        const { limit, offset, page, order, ...query } = conditions;

        // 拼装分页返回参数
        const result: Paginator<T> = {
            items: [],
            total: 0,
            limit,
            offset: 0,
            page: 1,
            pages: 0,
        };

        // 拼装查询配置参数
        options.sort = order;
        options.limit = limit;

        // 处理起始位置
        if (offset !== undefined) {
            result.offset = offset;
            options.skip = offset;
        } else if (page !== undefined) {
            result.page = page;
            options.skip = (page - 1) * limit;
            result.pages = Math.ceil(result.total / limit) || 1;
        } else {
            options.skip = 0;
        }

        try {
            // 获取分页数据
            result.items = await this.findAllAsync(query, projection, options);
            // 获取总条数
            result.total = await this.count(query);
            // 返回分页结果
            return Promise.resolve(result);
        } catch (e) {
            BaseRepository.throwMongoError(e);
        }
    }

    /**
     * @description 获取单条数据
     * @param {*} conditions
     * @param {(Object | string)} [projection]
     * @param {({
     *     lean?: boolean;
     *     populates?: ModelPopulateOptions[] | ModelPopulateOptions;
     *     [key: string]: any;
     *   })} [options]
     * @returns {QueryItem<T>}
     */
    public findOne(conditions: AnyType, projection?: object | string, options: {
        lean?: boolean;
        populates?: ModelPopulateOptions[] | ModelPopulateOptions;
        [key: string]: AnyType;
    } = {}): QueryItem<T> {
        return this.model.findOne(conditions, projection || {}, options);
    }

    public findOneAsync(conditions: AnyType, projection?: object | string, options: {
        lean?: boolean;
        populates?: ModelPopulateOptions[] | ModelPopulateOptions;
        [key: string]: AnyType;
    } = {}): Promise<T | null> {
        try {
            const { populates = null, ...option } = options;
            const docsQuery = this.findOne(conditions, projection || {}, option);
            return this.populates<T>(docsQuery, populates).exec();
        } catch (e) {
            BaseRepository.throwMongoError(e);
        }
    }

    /**
     * @description 根据id获取单条数据
     * @param {(string)} id
     * @param {(Object | string)} [projection]
     * @param {({
     *     lean?: boolean;
     *     populates?: ModelPopulateOptions[] | ModelPopulateOptions;
     *     [key: string]: any;
     *   })} [options={}]
     * @returns {QueryItem<T>}
     */
    public findById(id: string, projection?: object | string, options: {
        lean?: boolean;
        populates?: ModelPopulateOptions[] | ModelPopulateOptions;
        [key: string]: AnyType;
    } = {}): QueryItem<T> {
        return this.model.findById(BaseRepository.toObjectId(id), projection, options)
    }

    public findByIdAsync(id: string, projection?: object | string, options: {
        lean?: boolean;
        populates?: ModelPopulateOptions[] | ModelPopulateOptions;
        [key: string]: AnyType;
    } = {}): Promise<T | null> {
        try {
            const { populates = null, ...option } = options;
            const docsQuery = this.findById(id, projection || {}, option);
            return this.populates<T>(docsQuery, populates).exec();
        } catch (e) {
            BaseRepository.throwMongoError(e);
        }
    }

    /**
     * @description 获取指定查询条件的数量
     * @param {*} conditions
     * @returns {Query<number>}
     */
    public count(conditions: AnyType): Query<number> {
        return this.model.count(conditions)
    }

    public countAsync(conditions: AnyType): Promise<number> {
        try {
            return this.count(conditions).exec();
        } catch (e) {
            BaseRepository.throwMongoError(e);
        }
    }

    /**
     * @description 创建一条数据
     * @param {Partial<T>} docs
     * @returns {Promise<DocumentType<T>>}
     */
    public async create(docs: Partial<T>): Promise<DocumentType<T>> {
        try {
            return await this.model.create(docs);
        } catch (e) {
            BaseRepository.throwMongoError(e);
        }
    }

    /**
     * @description 删除指定数据
     * @param {(any)} id
     * @param {QueryFindOneAndRemoveOptions} options
     * @returns {QueryItem<T>}
     */
    public delete(
        conditions: AnyType,
        options?: QueryFindOneAndRemoveOptions,
    ): QueryItem<T> {
        return this.model.findOneAndDelete(conditions, options);
    }

    public async deleteAsync(
        conditions: AnyType,
        options?: QueryFindOneAndRemoveOptions,
    ): Promise<DocumentType<T>> {
        try {
            return await this.delete(conditions, options).exec();
        } catch (e) {
            BaseRepository.throwMongoError(e);
        }
    }

    /**
     * @description 删除指定id数据
     * @param {(any)} id
     * @param {QueryFindOneAndRemoveOptions} options
     * @returns {Query<FindAndModifyWriteOpResultObject<DocumentType<T>>>}
     */
    public deleteById(
        id: string,
        options?: QueryFindOneAndRemoveOptions,
    ): Query<FindAndModifyWriteOpResultObject<DocumentType<T>>> {
        return this.model.findByIdAndDelete(BaseRepository.toObjectId(id), options);
    }

    public async deleteByIdAsync(
        id: string,
        options?: QueryFindOneAndRemoveOptions,
    ): Promise<FindAndModifyWriteOpResultObject<DocumentType<T>>> {
        try {
            return await this.deleteById(id, options).exec();
        } catch (e) {
            BaseRepository.throwMongoError(e);
        }
    }

    /**
     * @description 更新指定id数据
     * @param {string} id
     * @param {Partial<T>} update
     * @param {QueryFindOneAndUpdateOptions} [options={ new: true }]
     * @returns {QueryItem<T>}
     */
    public update(id: string, update: Partial<T>, options: QueryFindOneAndUpdateOptions = { new: true }): QueryItem<T> {
        return this.model.findByIdAndUpdate(BaseRepository.toObjectId(id), update, options);
    }

    async updateAsync(id: string, update: Partial<T>, options: QueryFindOneAndUpdateOptions = { new: true }): Promise<DocumentType<T>> {
        try {
            return await this.update(id, update, options).exec();
        } catch (e) {
            BaseRepository.throwMongoError(e);
        }
    }

    /**
     * @description 删除所有匹配条件的文档集合
     * @param {*} [conditions={}]
     * @returns {Promise<WriteOpResult['result']>}
     */
    public clearCollection(conditions: AnyType = {}): Promise<WriteOpResult['result']> {
        try {
            return this.model.deleteMany(conditions).exec();
        } catch (e) {
            BaseRepository.throwMongoError(e);
        }
    }

    /**
     * @description 填充其他模型
     * @private
     * @template D
     * @param {DocumentQuery<D, DocumentType<T>, {}>} docsQuery
     * @param {(ModelPopulateOptions | ModelPopulateOptions[] | null)} populates
     * @returns {DocumentQuery<D, DocumentType<T>, {}>}
     */
    private populates<D>(
        docsQuery: DocumentQuery<D, DocumentType<T>, {}>,
        populates: ModelPopulateOptions | ModelPopulateOptions[] | null): DocumentQuery<D, DocumentType<T>, {}> {
        if (populates) {
            [].concat(populates).forEach((item: ModelPopulateOptions) => {
                docsQuery.populate(item);
            });
        }
        return docsQuery;
    }
}

这个代码很长。我们在说明一下做了什么:

  1. 首先一样,创建一个抽象类,携带泛型,它接受类型参数T extends BaseModel。这就是TypeScript的高级类型。在这里,我们明确地说 T extends BaseModel,这样我们只能将实际的领域模型类传递给这个基本服务,这是一个安全行为,你可以通过TypeScript来防止将ANY作为类型参数传递。
  2. 声明一个名为model的受保护字段,其类型为ReturnModelType<AnyParamConstructor<T>>。 实际上,ReturnModelType<AnyParamConstructor<T>>只是mongoose.model()将返回的类型。 那么是不是Model<T> ?是的。 但是Model<T>期望T extends mongoose.Document的接口。 使用typegoose,这里的所有内容都是类,因此我们无法真正使用Model提供的方法。 另一个想法是使模型受到保护,这意味着只有子类才能访问此字段,因此我们不会在应用程序的任何其他层(例如控制器层)中公开userService.model`
  3. 设置一些通用的方法来包裹mongoose.Model的方法并返回适当的类型。你可能已经注意到,每种方法都有两个版本。 第一个版本返回一个DocumentQuery,它使你可以链接方法以进一步:filter, project, 和一些其他东西,比如populatelean。 第2版​​(异步版)可在你不关心任何其他可链接方法而只想快速获取数据的情况下提供帮助。异步版本还将具有错误处理程序,我们将在其中引发MongoErrorInternalServerErrorException。 使用toObjectId将字符串转换成Types.ObjectId类型。

如果愿意,你可以抽象更多方法,但对我来说,这些方法在大多数情况下都已经够用了。

现在有基础模型和集成服务,我们来创建一个用户模型:

mkdir src/models/user // 存放用户的数据模型
touch src/models/index.ts // 用户模型导出索引
touch src/models/user.model.ts // 用户模型文件
touch src/models/user.repository.ts // 用户服务文件
touch src/models/user.module.ts // 用户模型模块文件

打开user.model.ts

import { prop, pre } from '@typegoose/typegoose';
import { Schema } from 'mongoose';
import { BaseModel } from '../base.model';
@pre<User>('save', function (next) {
    const now = new Date();
    (this as User).updated_at = now;
    next();
})
export class User extends BaseModel {
   get isAdvanced(): boolean {
        // 积分高于 700 则认为是高级用户
        return this.score > 700 || this.is_star;
    }

    @prop()
    name: string;

    @prop({
        index: true,
        unique: true,
        type: Schema.Types.String,
    })
    loginname: string;

    @prop({
        select: false,
        type: Schema.Types.String,
    })
    pass: string;

    @prop({
        index: true,
        unique: true,
        type: Schema.Types.String,
    })
    email: string;
}
  1. 这里大致说一下@prop(options: object)
    • required:boolean 必填
    • index: boolean 索引
    • unique: boolean 唯一
    • default: any 默认值
    • _id: boolean 为子文档创建标识
    • type: mongoose.Schema.Types 类型
    • select: boolean 要检索不带此属性的数据
    • ref: Class | string 用于引用的类
    • ...还有很多属性,基本上都是和mongoose配置一样
  2. @arrayProp设置数组
  3. 设置虚拟属性,直接使用getter/setter对应的就是get/set
  4. 设置createIndex使用@index,用法和Schema.index一样
  5. 使用@pre设置Schema.pre (不能使用箭头函数)
  6. 使用@post设置Schema.post (不能使用箭头函数)
  7. 使用@plugin设置Schema.plugin

打开user.repository.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { User } from './user.model';
import { ReturnModelType, DocumentType } from '@typegoose/typegoose';
import { BaseRepository } from '../base.repository';

/**
 * 用户实体
 */
export type UserEntity = User;
/**
 * 用户模型
 */
export type UserModel = ReturnModelType<typeof User>;
/**
 * 用户模型名称
 */
export const userModelName = User.modelName;
@Injectable()
export class UserRepository extends BaseRepository<User>{
    constructor(@InjectModel(User.modelName) private readonly _userModel: UserModel) {
        super(_userModel);
    }

    async create(docs: Partial<User>): Promise<DocumentType<User>> {
        docs.name = docs.loginname;
        const user = await super.create(docs);
        return user.save();
    }

    /*
    * 根据邮箱,查找用户
    * @param {String} email 邮箱地址
    * @param {Boolean} pass 启用密码
    * @return {Promise[user]} 承载用户的 Promise 对象
    */
    async getUserByMail(email: string, pass: boolean): Promise<User> {
        let projection = null;
        if (pass) {
            projection = '+pass';
        }
        return super.findOneAsync({ email }, projection);
    }

    /*
    * 根据登录名查找用户
    * @param {String} loginName 登录名
    * @param {Boolean} pass 启用密码
    * @return {Promise[user]} 承载用户的 Promise 对象
    */
    async getUserByLoginName(loginName: string, pass: boolean): Promise<User> {
        const query = { loginname: new RegExp('^' + loginName + '$', 'i') };
        let projection = null;
        if (pass) {
            projection = '+pass';
        }
        return super.findOneAsync(query, projection);
    }

    /*
    * 根据 githubId 查找用户
    * @param {String} githubId 登录名
    * @return {Promise[user]} 承载用户的 Promise 对象
    */
    async getUserByGithubId(githubId: string): Promise<User> {
        const query = { githubId };
        return super.findOneAsync(query);
    }
}

我只需要继承BaseRepository,书写特定的快捷方法即可,并导出UserEntityUserModel类型,模型名称userModelName

接下来打开user.module.ts

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UserRepository } from './user.repository';
import { User } from './user.model';

@Module({
    imports: [
        MongooseModule.forFeature([{ name: User.modelName, schema: User.schema }]),
    ],
    providers: [UserRepository],
    exports: [UserRepository],
})
export class UserModelModule { }

这里重要的一点是我们调用MongooseModule.forFeature并传递模型数组。 MongooseModule.forFeature并将获取当前的mongoose.Connection并添加传入的模型,然后将这些模型提供给NestJS的IoC容器(用于依赖注入)。 现在,你可以看到schema和modelName很重要,并且BaseModel在这里有很大帮助。

导出索引index.ts

export * from './user.module';
export * from './user.repository';

现在我们在业务模块auth里面使用UserModelModule完成注册登出等操作

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { LocalStrategy } from './passport/local.strategy';

import { AuthSerializer } from './passport/auth.serializer';
import { GithubStrategy } from './passport/github.strategy';
import { UserModelModule } from 'src/models';

@Module({
  imports: [UserModelModule],
  providers: [
    AuthService,
    AuthSerializer,
    LocalStrategy,
    GithubStrategy,
  ],
  controllers: [AuthController],
})
export class AuthModule { }

AuthService中使用UserRepository:

import { Injectable, Logger } from '@nestjs/common';
import { UserRepository } from 'src/models';

@Injectable()
export class AuthService {
    private readonly logger = new Logger(AuthService.name, true);
    constructor(
        private readonly userRepository: UserRepository,
    ) { }
}

即使这只是一个最小的示例,但我希望它向你展示如何利用TypeScript和Typegoose提取一些基础知识以加快开发过程。 此外,你甚至可以拥有一个BaseController,该Controller拥有一个受保护的baseService,它将涵盖您的基本CRUD功能。 基本就已经完结了,为了这个东西我花了很多时间和经历去挖坑,这里记录一下挖坑总结,这原因是typegoose的完整的栗子太少。

完整栗子:传送门

今天就到这里吧,伙计们,玩得开心,祝你好运