node.jstypescriptsequelize.jstypedi

I'd like to inject Repositories container at Service constructor using typedi


I'd like to inject Repositories at my UserService.

But i'm not sure how to do that. I'm using typescript, typedi and sequelize.

I think, the Service is loaded more fast than loaders.

When I try to inject my Repositories which I set at database loader, the error occur.

The error like this : ServiceNotFoundError: Service with "repositories" identifier was not found in the container. Register it before usage via explicitly calling the "Container.set" function or using the "@Service()" decorator.

So, I checked "userRepo" with console.log and the result was undefined. I also checked Container.get('repositories') at CreateUser meathod, it loaded collectly. I mean, I can get my Container instance.

I just can't load repositories instance at constructor.

What should I do to load repositories at constructor? Should I change sequelize to typeorm to load this?

// ** UserService.ts **
import { Inject, Service } from 'typedi';
import { UserRepository } from '../repositories/user.repository';
import { UserCreationAttributes } from '../models/interface/User.interface';
import { User, UserModel, UserStatic } from '../models/User';

@Service()
export default class UserService {
  constructor(@Inject('repositories') private userRepo: UserRepository) {}

  public async CreateUser(userData: UserCreationAttributes): Promise<boolean> {
    try {
      await this.userRepo.create(userData);
      return true;
    } catch (err) {
      console.log(err);
      return false;
    }
  }
}
// ** Database Loader **
import { Sequelize } from 'sequelize';
import config from '../config';
import Logger from './logger';
import { UserStatic } from '../models/User';
import { FeedStatic } from '../models/Feed';
import { CommentStatic } from '../models/Comment';
import { VerificationStatic } from '../models/Verification';
import { initializeModels } from '../models';
import { initializeRepositories, Repositories } from '../repositories';
import { Container } from 'typedi';

export interface Models {
  User: UserStatic;
  Feed: FeedStatic;
  Comment: CommentStatic;
  Verification: VerificationStatic;
}

export default async function loadSequelize() {
  const sequelize = new Sequelize(
    config.database,
    config.databaseUsername,
    config.databasePassword,
    {
      host: config.databaseHost,
      port: config.databasePort,
      dialect: 'postgres',
    },
  );

  try {
    await sequelize.authenticate();
    const models: Models = initializeModels(sequelize);
    const repositories: Repositories = initializeRepositories(models);
    await sequelize.sync({ force: true });

    // This part might be loaded after services were loaded
    Container.set('models', models);
    Container.set('repositories', repositories);


    console.log('load finish');
  } catch (err) {
    Logger.error(err);
  }
}

// ** ./repositories/index.ts **
import { Models } from '../loaders/database';
import { UserRepository } from './user.repository';

export interface Repositories {
  UserRepository: UserRepository;
}
export const initializeRepositories = (models: Models): Repositories => {
  const usersRepository = new UserRepository(models.User);
  const repositories: Repositories = {
    UserRepository: usersRepository,
  };
  return repositories;
};
// ** ./repositories/base.repository.ts **
import { Model, BuildOptions, FindOptions } from 'sequelize/types';
import { IFilter } from './filter/base.filter';

export type RichModel = typeof Model & {
  new (values?: Record<string, unknown>, options?: BuildOptions): Model;
};

export interface IMeta {
  globalCount: number;
  countAfterFiltering: number;
}

export interface IWithMeta<M extends Model> {
  meta: IMeta;
  data: M[];
}

export abstract class BaseRepository<
  M extends Model,
  C extends object,
  F extends IFilter = IFilter
> {
  constructor(public _model: RichModel, private filterFactory: new () => F) {}

  private async getCount(where?: Record<string, unknown>): Promise<number> {
    const count = await this._model.count({ where });
    return count;
  }

  async getAll(params?: FindOptions, filter?: F): Promise<IWithMeta<M>> {
    const { from: offset, count: limit } = filter || {};
    const result = await this._model.findAndCountAll({
      order: [['id', 'ASC']],
      offset: offset,
      limit: limit,
      ...params,
    });

    const globalCount = await this.getCount();
    const countAfterFiltering = ((result.count as unknown) as Record<
      string,
      unknown
    >[]).length;

    return {
      meta: { globalCount, countAfterFiltering },
      data: result.rows as M[],
    };
  }

  async getById(id: string | number): Promise<M> {
    const result = await this._model.findByPk(id);
    return result as M;
  }

  async get(where: Record<string, unknown>): Promise<M> {
    const result = await this._model.findOne({ where });
    return result as M;
  }

  async updateById(id: string | number, data: C): Promise<M> {
    const result = await this._model.update(data, {
      where: { id },
      returning: true,
    });

    const [, models] = result;

    return models[0] as M;
  }

  async deleteById(id: string | number): Promise<void> {
    await this._model.destroy({
      where: { id },
    });
  }

  async create(data: C): Promise<M> {
    const model = await this._model.create(data);
    return (model as unknown) as M;
  }
}
// ** ./repositories/user.repository.ts
import { BaseRepository, IWithMeta, RichModel } from './base.repository';
import { UserModel, UserStatic } from '../models/User';
import { UserCreationAttributes } from '../models/interface/User.interface';
import { IFilter } from './filter/base.filter';
import { UserFilter } from './filter/user.filter';
import { Service } from 'typedi';

@Service()
export class UserRepository extends BaseRepository<
  UserModel,
  UserCreationAttributes,
  IFilter
> {
  constructor(private model: UserStatic) {
    super(<RichModel>model, IFilter);
  }

  async getAllUsers(): Promise<IWithMeta<UserModel>> {
    const users = await this.getAll();
    return users;
  }

  async getOneByFilter({
    email,
    password,
  }: UserFilter): Promise<UserModel | null> {
    const user = await this.model.findOne({
      where: {
        email,
        password,
      },
    });
    return user;
  }

  async getAdminOneByFilter({
    email,
    password,
  }: UserFilter): Promise<UserModel | null> {
    const user = await this.model.findOne({
      where: {
        email,
        password,
        isAdmin: true,
      },
    });
    return user;
  }
}

Solution

  • First of all, try to put your initializeRepositories call right after the reflect-metadata call, to test if the execution order will impact. You don't need to call inside the database connection file.

    From what I saw at the old docs, to use Container.set to set all of your repos, you need to provide an array containing objects with id for them.

    Container.set([{ id: 'userRepository', value: new UserRepository() }])

    If you just need to set a single repository:

    Container.set('userRepository', new UserRepository())

    In both cases above, you use like: Container.get('userRepository');

    If you want to keep the reference as repositories your initializeRepositories method is ok, but the usage must be like:

    @Inject('repositories') repositories: { UserRepository: UserRepository}; 
    
    repositories.UserRepository
    

    If you don't need to set your repositories from that method you can just put a decorator @Service('userRepository') in your UserRepository class and it will work.