TypeScript - 带有 Sequelize 的存储库模式

Posted

技术标签:

【中文标题】TypeScript - 带有 Sequelize 的存储库模式【英文标题】:TypeScript - Repository pattern with Sequelize 【发布时间】:2021-11-02 04:24:13 【问题描述】:

我正在将我的 Express API 模板转换为 TypeScript,但我在存储库中遇到了一些问题。

使用 javascript,我会做这样的事情:

export default class BaseRepository 
  async all() 
    return this.model.findAll();
  

  // other common methods

import BaseRepository from './BaseRepository';
import  User  from '../Models';

export default class UserRepository extends BaseRepository 
  constructor() 
    super();
    this.model = User;
  

  async findByEmail(email) 
    return this.model.findOne(
      where: 
        email,
      ,
    );
  

  // other methods

现在,使用 TypeScript,问题在于它不知道 this.model 的类型,我无法将具体模型传递给 BaseRepository,因为它是一个抽象。我发现sequelize-typescript 导出了一个ModelCtor,它声明了所有静态模型方法,如findAll、create 等,我还可以使用另一个sequelize-typescript 导出Model 来正确注释返回类型。

所以,我最终这样做了:

import  Model, ModelCtor  from 'sequelize-typescript';

export default abstract class BaseRepository 
  protected model: ModelCtor;

  constructor(model: ModelCtor) 
    this.model = model;
  

  public async all(): Promise<Model[]> 
    return this.model.findAll();
  

  // other common methods

import  Model  from 'sequelize-typescript';
import BaseRepository from './BaseRepository';
import  User  from '../Models';

export default class UserRepository extends BaseRepository 
  constructor() 
    super(User);
  

  public async findByEmail(email: string): Promise<Model | null> 
    return this.model.findOne(
      where: 
        email,
      ,
    );
  

  // other methods

好的,这行得通,TypeScript 不会抱怨诸如 findOnecreate 之类的方法不存在,但这会产生另一个问题。

现在,例如,每当我从存储库中获得 User 时,如果我尝试访问其属性之一,例如 user.email,TypeScript 就会抱怨该属性不存在。当然,因为Model这个类型不知道每个型号的具体情况。

好吧,那就是 treason 泛型。

现在 BaseRepository 使用通用的 Model 类型,方法也使用该类型:

export default abstract class BaseRepository<Model> 
  public async all(): Promise<Model[]> 
    return Model.findAll();
  

  // other common methods

具体类将适当的模型传递给泛型类型:

import BaseRepository from './BaseRepository';
import  User  from '../Models';

export default class UserRepository extends BaseRepository<User> 
  public async findByEmail(email: string): Promise<User | null> 
    return User.findOne(
      where: 
        email,
      ,
    );
  

  // other methods

现在 IntelliSense 可以正确亮起,它同时显示抽象类和具体类方法以及模型属性(例如 user.email)。

但是,正如您所想象的那样,这会导致更多问题。

BaseRepository 内部,方法使用Model 泛型类型,TypeScript 抱怨'Model' only refers to a type, but is being used as a value here。不仅如此,TypeScript 也(再次)不知道模型中的静态方法存在,例如 findAllcreate 等。

另一个问题是,在抽象类和具体类中,由于方法不再使用this,ESLint 期望方法是静态的:Expected 'this' to be used by class async method 'all'。好的,我可以在整个文件中忽略此规则,错误就消失了。将所有方法设置为静态会更好,因此我不必实例化存储库,但也许我做梦太多了。

值得一提的是,虽然我可以使用 // @ts-ignore 来消除这些错误,但当我执行此操作时,它不起作用:TypeError: Cannot read property 'create' of undefined\n at UserRepository.&lt;anonymous&gt;

我研究了很多,试图让所有方法都是静态的,但是静态方法不能引用泛型类型(因为它被认为是实例属性),尝试了一些变通方法,尝试在@的构造函数中传递具体模型987654352@ 以及使用泛型类型的类,但到目前为止似乎没有任何效果。

如果您想检查代码:https://github.com/andresilva-cc/express-api-template/tree/main/src/App/Repositories

编辑:

找到这个:Sequelize-Typescript typeof model

好的,我从那篇帖子中删除了一些不必要的代码,这有点用:

import  Model  from 'sequelize-typescript';

export default abstract class BaseRepository<M extends Model> 
  constructor(protected model: typeof Model) 

  public async all(attributes?: string[]): Promise<M[]> 
    // Type 'Model<, >[]' is not assignable to type 'M[]'.
    // Type 'Model<, >' is not assignable to type 'M'.
    // 'Model<, >' is assignable to the constraint of type 'M', but 'M' could be instantiated with a different subtype of constraint 'Model<any, any>'.
    return this.model.findAll(
      attributes,
    );
  
import BaseRepository from './BaseRepository';
import  User  from '../Models';

export default class UserRepository extends BaseRepository<User> 
  constructor() 
    super(User);
  

我的意思是,如果我输入一些 // @ts-ignore,它至少会执行,并且 IntelliSense 会完美亮起,但 TypeScript 会抱怨。

【问题讨论】:

【参考方案1】:

我们遇到了同样的问题。解决方案是使用抽象存储库类实现的接口声明返回类型。

接口代码:

export type RepoResult<M> = Promise<Result<M | undefined, RepoError | undefined>>;

export interface IRepo<M> 
    save(model: M): RepoResult<M>;
    findById(id: string): RepoResult<M>;
    search(parameterName: string, parameterValue: string, sortBy: string, order: number, pageSize: number, pageNumber: number): RepoResult<M[]>;
    getAll(): RepoResult<M[]>;
    deleteById(id: string): RepoResult<M>;
    findByIds(ids: string[]): RepoResult<M[]>;
    deleteByIds(ids: string[]): RepoResult<any>;
;

抽象类的代码:

export abstract class Repo<M extends sequelize.Model> implements IRepo<M> 
            protected Model!: sequelize.ModelCtor<M>;
            constructor(Model: sequelize.ModelCtor<M>) 
                this.Model = Model;
            
        
            public async save(doc: M) 
                try 
                    const savedDoc = await doc.save();
                    return Result.ok(savedDoc);
                 catch (ex: any) 
                    logger.error(ex);
                    return Result.fail(new RepoError(ex.message, 500));
                
            
        
            public async findById(id: string) 
                try 
                    const doc = await this.Model.findOne(where: 
                        id: id
                    );
                    if (!doc) 
                        return Result.fail(new RepoError('Not found', 404));
                    
        
                    return Result.ok(doc);
                 catch (ex: any) 
                    return Result.fail(new RepoError(ex.message, 500));
                
            
        

希望对您有所帮助。祝你有美好的一天:)

编辑: 结果是一个如下所示的类:

export class Result<V, E> 
public isSuccess: boolean;
public isFailure: boolean;
private error: E;
private value: V;

private constructor(isSuccess: boolean, value: V, error: E) 
    if (isSuccess && error) 
        throw new Error('Successful result must not contain an error');
     else if (!isSuccess && value) 
        throw new Error('Unsuccessful error must not contain a value');
    
    
    this.isSuccess = isSuccess;
    this.isFailure = !isSuccess;
    this.value = value;
    this.error = error;


public static ok<V>(value: V): Result<V, undefined> 
    return new Result(true, value, undefined);


public static fail<E>(error: E): Result<undefined, E> 
    return new Result(false, undefined, error);


public getError(): E 
    if (this.isSuccess) 
        throw new Error('Successful result does not contain an error');
    

    return this.error;


public getValue(): V 
    if (this.isFailure) 
        throw new Error('Unsuccessful result does not contain a value');
    

    return this.value;


RepoError 类:

type RepoErrorCode = 404 | 500;

export class RepoError extends Error 
    public code: RepoErrorCode;
    constructor(message: string, code: RepoErrorCode) 
        super(message);
        this.code = code;
    

RepoResult 类型:

export type RepoResult<M> = Promise<Result<M | undefined, RepoError | undefined>>;

您可以在以下链接中找到有关该模式的更多信息: https://khalilstemmler.com/articles/enterprise-typescript-nodejs/functional-error-handling/

【讨论】:

这里的repoError和结果是什么?您能否在界面中添加这些导入? 完成!很高兴,如果它有帮助:)

以上是关于TypeScript - 带有 Sequelize 的存储库模式的主要内容,如果未能解决你的问题,请参考以下文章

TypeScript - 带有 Sequelize 的存储库模式

sequelize-typescript - 当使用带有 `tsc` 编译器(带有 NestJS)的 glob 配置时,无法解析模型模块

Sequelize Typescript Sequelize Typescript TypeError:没有'new'就不能调用类构造函数模型

Sequelize-typescript 'HasManyCreateAssociationMixin' 不是函数

Sequelize beforeConnect 钩子与 sequelize-typescript 未运行

使用 Typescript 在 Sequelize 模型中创建实例方法