TypeScript - 带有 Sequelize 的存储库模式

TypeScript - Repository pattern with Sequelize

我正在将我的 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 正确注释 return 类型。

所以,我最终这样做了:

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不知道每个型号的具体情况。

好吧,那是 叛国 仿制药。

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

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

  // other common methods
}

并且具体 classes 将适当的模型传递给通用类型:

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 正确亮起,它显示了抽象和具体的 classes 方法和模型属性(例如 user.email)。

但是,如您所想,这会导致更多问题。

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

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

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

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

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

编辑:

找到这个:

好的,我从 post 中删除了一些不必要的代码,效果还不错:

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 会抱怨。

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

接口代码:

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>;
};

摘要代码 class:

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));
                }
            }
        }

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

编辑: 结果是 class,看起来像这样:

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 class:

type RepoErrorCode = 404 | 500;

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

回购结果类型:

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

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