Express + Typescript + Objection pass model to super class using generic type not working - node.js

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

Related

Nestjs: calling service functions from Model / Entity with sequelize hooks

In NestJS, I have to use a module service into an entity/model to populate data into elastic-search index. populating elastic search index logic is written in Job.service.ts.
I want to call that onCreate method from Job.service.ts from sequelize hooks present in models.
Here is code for Job.ts model/entity -
import { Table, Model, Column, AutoIncrement, PrimaryKey } from "sequelize-typescript";
#Table({ schema: "job", tableName: "job" })
export class Job extends Model<Job> {
#AutoIncrement
#PrimaryKey
#Column
id: number;
#Column
title: string;
#AfterCreate
static async jobAfterCreate(instance, options) {
// <--- need to call job service onCreate method here
}
#AfterUpdate
static async jobAfterUpdate() {}
#AfterDestroy
static async jobAfterDestroy() {}
}
and here is code for Job.service.ts -
//imports not added
#Injectable()
export class JobService {
constructor(
#Inject("SEQUELIZE")
private readonly sequelizeInstance: Sequelize,
#Inject(forwardRef(() => ElasticsearchService))
private readonly elasticsearchService: ElasticsearchService,
#InjectModel(Job)
private jobModel: typeof Job
) {}
// here will write logic for updating elastic search index
async onCreate(instance, options){
console.log("ON CREATE INSTANCE:", instance);
console.log("ON CREATE OPTIONS:", options);
}
async onDestroy(instance, options){
console.log("ON DESTROY INSTANCE:", instance);
console.log("ON DESTROY OPTIONS:", options);
}
}
I tried injecting service into Job model but it did not worked.
And I cannot write elastic search logic inside model directly because for that I need ElasticsearchService.
The Solution is To Override the provider
The primary way to inject information into the models is by overriding the injection behavior.
First, you would need to add a static property referencing the service in your model.
I am going to use the event emitter as an example here.
Your Model Class
import {Model, Table, Column, AfterCreate} from "sequelize-typescript";
import { EventEmitter2 } from "#nestjs/event-emitter";
#Table()
export class SomeModel extends <SomeModel> {
// this would be your referencing
public static EventEmitter: EventEmitter2;
#Column
public someColumn: string;
#AfterCreate
public static triggerSomeEvent(instance: SomeModel) {
SomeModel.EventEmitter.emit('YourEvent', instance);
}
}
The module where you are going to use the model
Now we are overriding the default injection process.
import { EntitiesMetadataStorage } from '#nestjs/sequelize/dist/entities-metadata.storage';
import {
getConnectionToken,
getModelToken,
SequelizeModule,
} from '#nestjs/sequelize';
import { EventEmitter2 } from '#nestjs/event-emitter';
// The provider override
const modelInjector: Provider = {
provide: getModelToken(AccountabilityPartnerModel, DEFAULT_CONNECTION_NAME),
useFactory: (connection: Sequelize, eventEmitter: EventEmitter2) => {
SomeModel.EventEmitter = eventEmitter;
if (!connection.repositoryMode) {
return SomeModel;
}
return connection.getRepository(SomeModelas any);
},
inject: [getConnectionToken(DEFAULT_CONNECTION_NAME), EventEmitter2],
};
// Updating the meta information of sequelize-typescript package to handle connection injection in to the model overridden.
EntitiesMetadataStorage.addEntitiesByConnection(DEFAULT_CONNECTION_NAME, [
SomeModel,
]);
// our custom module being used rather than the Sequelize.forFeature([SomeModel])
const someModelModule: DynamicModule = {
module: SequelizeModule,
providers: [modelInjector],
exports: [modelInjector],
};
#Module({
imports: [someModelModule],
providers: [SomeService],
})
export class SomeModule {
}
Inject your model into your service as you would do using Sequlize.forFeature and InjectModel indicated as below.
#Injectable()
export class SomeService {
constructor(#InjectModel(SomeModel) someModel: typeof SomeModel) {}
public someFunction(data: any) {
this.someModel.EventEmitter.emit('YourEvent', data);
}
}

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/

NestJs Dependency injection get multiple instance in a single class

I have a class Item below
#Injectable()
export class Item {
name: string;
}
I am injecting Item class into ItemService
export class ItemService {
constructor(private readonly item: Item) {}
save(items) {
items.forEach((data) => {
this.item.name = data.name;
this.item.save();
});
}
}
The problem here is I am not getting new instance of item for every loop. How can I achieve this with dependency injection in nestjs.
I am not getting new instance of item for every loop.
Blockquote
That's what dependency injection is, you inject the already created object of class if available, It follows the singleton pattern.
How can I achieve this with dependency injection in nestjs.
By creating a new object in each iteration;
import item from 'Item'
export class ItemService {
constructor() {}
save(items) {
items.forEach((data) => {
item = new Item()
item.name = data.name;
item.save();
});
}
}

What is the right way to export the class to be extended from other classes NodeJs?

Base class looks like this:
class BaseReport {
constructor(reportName) {
this.reportName = reportName;
}
async generateReport(accountId, request) {
let results = await this.getDataFromDb(request);
results = this.formatResults(results);
return updatedResults;
}
formatResults(data) {
//Some logig here
return result
}
getDataFromDb(request) {
//Logic to get data from database
return errorRequest;
}
}
module.exports = BaseReport;
The I have another class that extends Base class:
const BaseReport = require("./base.service");
class DataReport extends BaseReport {
constructor() {
super('dataReport');
}
formatResults(data) {
//Logic to format results in a different way
return data;
}
}
module.exports = new DataReport();
So far everything works well. DataReport class doesn't need to implement the method getDataFromDB because it is inherited from BaseReport class.
The problem starts when I am writing unit/integration tests.
When I try to stub function getData() it is not working (it is not being stubbed but instead it is calling the method and making the database call.
I understand WHY is happening... In BaseReport class I am exporting the class itself. So when I create an object on my unit tests and try to stub the function, that is not the same method and object which is initialized when the application is running. Every time you use the keyword "new" creates a new object and has its own methods.
If I want to stub a method from DataReport class it works fine because in there I am exporting an object of that class (module.exports = new DataReport();). In this case, it can only exist one copy of the object and so only one copy of the methods as well.
Now I can not do the same for BaseReport class because my understanding is that you CAN NOT extend BaseReport class if I exported a new object (module.exports = new BaseReport();).
What is the proper way to implement this in order to also have the unit tests working??

Node.js and sequelize-typescript - data access objects and business objects

I am using the sequelize-typescript in my Node.js service
I have the Category class which maps to category table
import { Model, Table, Column } from "sequelize-typescript";
#Table
export class Category extends Model<Category>{
#Column
name: string
}
I also have CategoryController and CategoryService
export class CategoryController {
...
async getAll(request: Request, response: Response) {
let categories = await this.categoryService.getCatergories();
response.json(categories)
}
}
export class CategoryService {
async getCatergories(): Promise<Category[]> {
let categories = await Category.findAll<Category>()
return categories
}
}
And everything is as it should be.
But returning a Category to the controller allows it to use the use the inherited methods from the model class like:
export class CategoryController {
...
async getAll(request: Request, response: Response) {
let categories = await this.categoryService.getCatergories();
// Remove associated row in the database
categories[0].destroy()
response.json(categories)
}
}
I was thinking to create a CategoryModel class like this:
export class CategoryModel {
id : number
name : string
}
And modify all methods in CategoryService to return CategoryModel instances instead of Category and rename Category to CategoryEntity
What is the best way to deal with such a problem?
Use toJSON() of Category instance to get "a JSON representation" of the instance.
See sequelize docs for more information: http://docs.sequelizejs.com/class/lib/model.js~Model.html#instance-method-toJSON
Additionally you could add an interface to achieve type safety for the return value of toJSON() instead of defining another class:
interface ICategory {
id: number;
name: string;
}
#Table
export class Category extends Model<Category> implements ICategory{
#Column
name: string
}
Using toJSON():
Category.findOne(result => {
const category: ICategory = result.toJSON();
});

Resources