Cannot extend base class with Mongoose and Typescript in NodeJS - node.js

I'm attempting to use Mongoose 4.5.4 and it's typings in NodeJS with Typescript (obviously) while utilizing the Repository Pattern.
RepositoryBase:
export class RepositoryBase<T extends mongoose.Document> implements IRead<T>, IWrite<T> {
private _model: mongoose.Model<mongoose.Document>;
constructor(schemaModel: mongoose.Model<mongoose.Document>) {
this._model = schemaModel;
}
create(item: T): mongoose.Promise<mongoose.model<T>> {
return this._model.create(item);
}
//......
//Additional CRUD methods excluded for brevity
}
UserRepository:
export class UserRepository extends RepositoryBase<IUserModel> {
constructor() {
super(UserSchema);
}
}
I have the following type error on the call to super() from UserRepository
[ts]
Argument of type '<U>(name: string) => IModelConstructor<U> & EventEmitter' is not assignable to parameter of type 'IModelConstructor<Document> & EventEmitter'.
Type '<U>(name: string) => IModelConstructor<U> & EventEmitter' is not assignable to type 'IModelConstructor<Document>'.
Property 'findById' is missing in type '<U>(name: string) => IModelConstructor<U> & EventEmitter'.
import UserSchema
Does anyone know why? My creation of my model (UserSchema) is very simple:
let model = mongooseConnection.model<IUserModel>("Users", UserSchema.schema);
I would much appreciate a push in the right direction.

Doing a simple export const model = mongooseConnection.model<IUserModel>("Users", UserSchema.schema); was enough to solve this typing issue.

Related

Express + Typescript + Objection pass model to super class using generic type not working

class BaseService<Models> {
public m: Models;
constructor(model: Models) {
this.m = model;
}
}
class MyService extends BaseService<UserModel>{
constructor() {
super(UserModel); //It is require new keyword
}
}
We are using knex.js with Objection.js models.
I'm able to run queries in MyService class.
e.g.
const user = await UserModel.query().findById(1);
But instead i want a base service that to set Model using generic type.
What i need from child class id.
super(UserModel); //It is not working
If i pass UserModel using new keyword then error removed but queries not working
super(new UserModel())
Typescript Playground Link
You tried to pass class to super class, but it expected instance of that class
class BaseService<Models> {
public m: Models;
constructor(model: Models) {
this.m = model;
}
}
class UserModel {}
class MyService extends BaseService<typeof UserModel>{
constructor() {
super(UserModel); //It is require new keyword
}
}
new MyService();

TypeScript - Repository pattern with Sequelize

I'm converting my Express API Template to TypeScript and I'm having some issues with the repositories.
With JavaScript, I would do something like this:
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
Now, with TypeScript, the problem is that it doesn't know the type of this.model, and I can't pass a concrete model to BaseRepository, because, well, it is an abstraction. I've found that sequelize-typescript exports a ModelCtor which declares all the static model methods like findAll, create, etc., and I also could use another sequelize-typescript export which is Model to properly annotate the return type.
So, I ended up doing this:
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
}
Ok, this works, TypeScript doesn't complain about methods like findOne or create not existing, but that generates another problem.
Now, for example, whenever I get a User from the repository, if I try to access one of its properties, like user.email, TypeScript will complain that this property does not exist. Of course, because the type Model does not know about the specifics of each model.
Ok, it's treason generics then.
Now BaseRepository uses a generic Model type which the methods also use:
export default abstract class BaseRepository<Model> {
public async all(): Promise<Model[]> {
return Model.findAll();
}
// other common methods
}
And the concrete classes pass the appropriate model to the generic type:
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
}
Now IntelliSense lights up correctly, it shows both abstract and concrete classes methods and the model properties (e.g. user.email).
But, as you have imagined, that leads to more problems.
Inside BaseRepository, where the methods use the Model generic type, TypeScript complains that 'Model' only refers to a type, but is being used as a value here. Not only that, but TypeScript also doesn't know (again) that the static methods from the model exist, like findAll, create, etc.
Another problem is that in both abstract and concrete classes, as the methods don't use this anymore, ESLint expects the methods to be static: Expected 'this' to be used by class async method 'all'. Ok, I can just ignore this rule in the whole file and the error is gone. It would be even nicer to have all the methods set to static, so I don't have to instantiate the repository, but maybe I'm dreaming too much.
Worth mentioning that although I can just silence those errors with // #ts-ignore, when I execute this, it doesn't work: TypeError: Cannot read property 'create' of undefined\n at UserRepository.<anonymous>
I researched a lot, tried to make all methods static, but static methods can't reference the generic type (because it is considered an instance property), tried some workarounds, tried to pass the concrete model in the constructor of BaseRepository along with the class using the generic type, but nothing seems to work so far.
In case you want to check the code: https://github.com/andresilva-cc/express-api-template/tree/main/src/App/Repositories
EDIT:
Found this: Sequelize-Typescript typeof model
Ok, I removed some unnecessary code from that post and that kinda works:
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);
}
}
I mean, if I put some // #ts-ignore it at least executes, and IntelliSense lights up perfectly, but TypeScript complains.
We faced the same problem. The solution was to declare returning types with an interface that an abstract repository class implements.
Code for the interface:
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>;
};
Code for the abstract 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));
}
}
}
Hope it helps. Have a nice day:)
EDIT:
Result is a class that looks like this:
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;
}
}
RepoResult type:
export type RepoResult<M> = Promise<Result<M | undefined, RepoError | undefined>>;
You can find more info on the pattern at the link below:
https://khalilstemmler.com/articles/enterprise-typescript-nodejs/functional-error-handling/

How to make parameter type-safe for method that uses Sequelize in TypeScript

I'm working with sequelize-typescript in a NestJS project and I have service classes that use sequelize models to run CRUD operations. This is an example:
import { Injectable, Inject } from "#nestjs/common"
import { Platform } from "./platform.model"
import { Game } from "../game/game.model"
import { QueryOptionsDto } from "./dto/queryOptions.dto"
import { FindOptions } from "sequelize/types"
import { PLATFORMS_REPOSITORY } from "src/core/constants"
#Injectable()
export class PlatformService {
constructor(
#Inject(PLATFORMS_REPOSITORY) private platformRepository: typeof Platform,
) {}
// the payload parameter is the problem here
public async addPlatform(payload) {
try {
const platform = await this.platformRepository.create(payload)
return platform
} catch (err) {
return Promise.reject(err)
}
}
}
But I can't assign any type to the payload parameter. Naturally, I want to make this type-safe, but TypeScript seems to want the type of the object passed to .create() to be the same type as the corresponding model and have all the same properties and methods. This is the error I get if I try to give it an object type with the properties it should have ({ name: string, year: number }):
Argument of type '{ name: string; year: number; }' is not assignable to parameter of type 'Platform'.
Type '{ name: string; year: number; }' is missing the following properties from type 'Platform': games, $add, $set, $get, and 33 more.
The only solution is to just leave the type as inferred any but this obviously defeats the purpose of using TypeScript.
Any suggestions?

TypeScript: Decorating a derived class with typed decorator function

I am trying to build an "Entity Framework"-like ORM library for node.js and MongoDB with TypeScript.
With this library, the consumer will be able to define a Model class (ex. Person) that will extend a Base class, and the class decorator will add additional data to the Person class (for example, instances array, that will contain all the Model's instances, and collection_name that will be the MongoDB collection name for the model, etc.).
Code in TypeScript playground.
So my first step was creating a Base class:
class Base<T>
{
static collection_name: string
static instances: Base<any>[]
_id: string
}
So the user will be able to define his model like so:
#decorator<Person>({collection_name: 'people'})
class Person extends Base<Person>
{
#field name
#field address
...
}
then I created a decorator class to set the collection_name and instances properties on the Person class:
function decorator<T>(config: { collection_name: string }) {
return function <T extends Base<T>>(Class: IClass<T>) {
Class.collection_name = config.collection_name;
Class.instances = [];
return Class
}
}
the decorator function receives the user-generated Class, and I am trying to create an interface that will describe the type of such class. I called it IClass:
interface IClass<T>
{
new(): Base<T>
instances: Base<T>[];
collection_name: string
}
new is the constructor (that returns a Base instance)
instances and collection_name are static properties of Base<T> and are non-static here (I'm not sure about this, is this right?)
However, when trying to define the user Model I get the following error:
#decorator<Person>({collection_name: 'people'}) // <==== ERROR HERE ===
class Person extends Base<Person>
{
}
Error:(23, 2) TS2345: Argument of type 'typeof Person' is not
assignable to parameter of type 'IClass>'.
Property 'instances' is missing in type 'typeof Person' but
Type 'typeof Person' is missing the following properties from type
required in type 'IClass>'.
It seems like the typescript compiler is ignoring the static members inherited from Base when checking the type of typeof Person.
How can I define the type of the Class property of the decorator function ?
The problem as jcalz points out is that your decorator is accepting a Class of a type that already has the static properties instances and collection_name. You need to use two different interfaces, one which is a type that simply constructs instances of T with the new(): T signature, and another that extends this interface to include the static properties your decorator will add.
class Base<T> {
static _id = 0;
private _id: number;
constructor () {
this._id = Base._id++;
}
}
interface BaseConstructor<T extends Base<T>> {
_id: number;
new(): T;
}
interface DecoratedBaseConstructor<T extends Base<T>> extends BaseConstructor<T> {
instances: T[];
collection_name: string;
}
function decorator<T extends Base<T>>(config: { collection_name: string }) {
return (baseConstructor: BaseConstructor<T>): DecoratedBaseConstructor<T> => {
const decoratedBaseConstructor = baseConstructor as Partial<DecoratedBaseConstructor<T>>;
decoratedBaseConstructor.collection_name = config.collection_name;
decoratedBaseConstructor.instances = [];
return decoratedBaseConstructor as DecoratedBaseConstructor<T>;
};
}
#decorator<Person>({collection_name: 'people'})
class Person extends Base<Person> {
name: string;
constructor () {
super();
this.name = 'foo';
}
}
With this approach, all of Base's static members must be public. Any static members of Base initialized in the decorator should go in the DecoratedBaseConstructor, and any remaining static members not initialized in the decorator should go in the BaseConstructor instead.
I assume that you use the generic type T in the Base class somehow in your actual code, but if you don't, you should remove the generic type from the Base class and everything else will still work the same.
Check out the above snippet in this playground.

Get file path of imported module

I'm writing a class decorator for my controllers. It looks like:
export function Controller<T extends { new(...args: any[]): {} }> (ctor: T) {
return class extends ctor {
public readonly name = name;
}
}
ctor is a constructor of a class decorated with #Controller.
Full path to the controller's file is src/modules/{module}/controllers/{ctrl}Controller.ts. I need to get parts in curly braces and concatenate them into {module}.{ctrl}.
To do so I need a filepath of module from which ctor is imported. How can I obtain it?
There is no way to get file path information from ctor parameter. It's just a function that was defined somewhere.
Basically, module and ctrl preferably have to be provided to controller class on registration, since the path is known at this moment, i.e.:
for (const filename of filenames) {
const Ctrl = require(filename).default;
const [moduleName, ctrlName] = parseCtrlFilename(filename);
Ctrl._module = moduleName;
Ctrl._name = ctrlName;
}
The only and hacky workarount is to get file path of a place where Controller was called. This is achieved by getting stacktrace, e.g:
const caller = require('caller-callsite');
export function Controller<T extends { new(...args: any[]): {} }> (ctor: T) {
const fullPath = caller().getFileName();
...
}
The problem is that it's the path where Controller is called:
.../foo.ts
#Controller
export class Foo {...}
.../bar.ts
import { Foo } from '.../foo.ts';
// fullPath is still .../foo.ts
export class Bar extends Foo {}
A less hacky and more reliable way is to provide file path explicitly from the module where it is available:
#Controller(__filename)
export class Foo {...}
There is import.meta proposal which is supported by TypeScript. It depends on Node project configuration because it works with esnext target:
#Controller(import.meta)
export class Foo {...}
import.meta that was passed to #Controller can be consumed as meta.__dirname.

Resources