This is my implementation:
Base repository:
export class Repository<T> {
repository: Model<T>;
constructor(
repository: Model<T, Record<string, never>, Record<string, never>>,
) {
this.repository = repository;
}
async find(filter: FilterQuery<T | any>, projection?: any): Promise<T[]> {
return this.repository.find(filter, projection);
}
async findById(
id: string,
projection?: ProjectionType<T>,
options?: QueryOptions<T>,
): Promise<T> {
return this.repository.findById(id, projection, options);
}
async findOne(
filter?: FilterQuery<T>,
projection?: ProjectionType<T>,
options?: QueryOptions<T>,
): Promise<T> {
return this.repository.findOne(filter, projection, options);
}
async create(doc: T | AnyObject | DocumentDefinition<T>): Promise<T> {
return this.repository.create(doc);
}
.
.
.
}
Entity repository:
export default class UserRepository extends Repository<UserDocument> {
constructor(@InjectModel(User.name) private userModel: Model<UserDocument>) {
super(userModel);
}
async findByEmail(email: string): Promise<UserDocument> {
return this.findOne({ email });
}
}
Service:
export default class UserService {
constructor(private readonly userRepository: UserRepository) {}
async createUser(email: string, password: string): Promise<UserDocument> {
const user = await this.userRepository.findByEmail(email);
if (user) {
throw new UserAlreadyExistsException();
}
const createdUser = await this.userRepository.create({ email, password });
return createdUser;
}
}
User module:
@Module({
imports: [
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
],
controllers: [],
providers: [UserRepository, UserService],
exports: [UserRepository, UserService],
})
export default class UserModule {}
Mongo schema:
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
export type UserDocument = HydratedDocument<User>;
@Schema({
versionKey: false,
timestamps: true,
toJSON: {
virtuals: true,
transform: (doc, ret) => {
delete ret._id;
},
},
})
export class User {
@Prop({
type: String,
required: true,
})
email: string;
@Prop({
type: String,
required: true,
})
password: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
Answer from avnigenc on Stack OverflowThe problem here comes from the @InjectRepository(User) : it injects an instance of Repository instead of the BaseRepository one.
I found this repository that provide a way to override the repository provided by the TypeORM module, but only for a specific entity.
However, we could adapt his approach to provide a generic repository instead :
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [buildCustomRepositoryProvider<User>(User), UserService],
})
export class UserModule {
}
With a helper file like this :
import { DataSource, Repository, UpdateResult } from 'typeorm';
import { Provider } from '@nestjs/common';
import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm';
import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type';
export interface BaseRepository<T> extends Repository<T> {
this: Repository<T>;
updateAndLog(id: string, data: any): Promise<UpdateResult>;
}
export function buildCustomRepositoryMethods<T>(): Pick<BaseRepository<T>, 'updateAndLog'> {
return {
async updateAndLog(id: string, data: any): Promise<UpdateResult> {
const entity = await this.findOne({ where: { id: id as any } });
const savedEntity = await this.update(id, data);
// log the data here
return savedEntity;
},
};
}
export function buildCustomRepositoryProvider<T>(entity: EntityClassOrSchema): Provider {
return {
provide: getRepositoryToken(entity),
inject: [getDataSourceToken()],
useFactory: (dataSource: DataSource) => {
// Override the default repository with a custom one
return dataSource.getRepository(entity).extend(buildCustomRepositoryMethods<T>());
},
};
}
Therefore, the @InjectRepository(User) will inject an instance of Repository<User> extended with the methods provided by the BaseRepository interface.
(Note : using the extend method here to create a custom repository as it's the recommended way since TypeORM 0.3, see here)
Try this soluion, I am using this solution in my application
import { Type } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ObjectLiteral, Repository } from 'typeorm';
interface IDataService<T extends ObjectLiteral> {
readonly repository: Repository<T>;
findById: (id: string | number) => Promise<T | null>;
}
type Constructor<I> = new (...args: any[]) => I;
export function DataService<T extends ObjectLiteral>(
entity: Constructor<T>,
): Type<IDataService<T>> {
class DataServiceHost implements IDataService<T> {
@InjectRepository(entity) public readonly repository: Repository<T>;
public async findById(id: string | number): Promise<T | null> {
return this.repository.findOneBy({ id } as any);
}
}
return DataServiceHost;
}
Extend your service from DataService class,
class YourService extends DataService<YourDatasebaseEntity>(YourDatasebaseEntity)
None of the above answers fixed the issue. I found an open issue on TypeORM GitHub relating to this problem.
For now, I fixed this with any type. I know it is not perfect but, it is working for now. I'm looking forward to any other solutions.
import { BaseEntity } from './base.entity';
import { Repository } from 'typeorm';
export class BaseService<Entity extends BaseEntity> {
constructor(private entitiesRepository: Repository<Entity>) {}
findById(id: any): Promise<Entity> {
return this.entitiesRepository.findOneBy({ id });
}
}
You need to decorate your BaseEntity class @Entity() as stated in the documentation : https://docs.nestjs.com/techniques/database#repository-pattern
// base.entity.ts
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity()
export class BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ default: false })
isDeleted: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}