Merge pull request #467 from Hexastack/fix/base-repository-with-strict-null-checks

Fix: base repository with strict null checks
This commit is contained in:
Med Marrouchi 2024-12-30 10:38:16 +01:00 committed by GitHub
commit e489bc57a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 46 additions and 35 deletions

View File

@ -81,7 +81,7 @@ export abstract class BaseRepository<
readonly model: Model<T>, readonly model: Model<T>,
private readonly cls: new () => T, private readonly cls: new () => T,
protected readonly populate: P[] = [], protected readonly populate: P[] = [],
protected readonly clsPopulate: new () => TFull = undefined, protected readonly clsPopulate?: new () => TFull,
) { ) {
this.registerLifeCycleHooks(); this.registerLifeCycleHooks();
} }
@ -98,8 +98,15 @@ export abstract class BaseRepository<
private registerLifeCycleHooks(): void { private registerLifeCycleHooks(): void {
const repository = this; const repository = this;
const hooks = LifecycleHookManager.getHooks(this.cls.name); const hooks = LifecycleHookManager.getHooks(this.cls.name);
if (!hooks) {
// eslint-disable-next-line no-console
console.warn(
`LifeCycleHooks has not been registered for ${this.cls.name}`,
);
return;
}
hooks?.validate.pre.execute(async function () { hooks.validate.pre.execute(async function () {
const doc = this as HydratedDocument<T>; const doc = this as HydratedDocument<T>;
await repository.preCreateValidate(doc); await repository.preCreateValidate(doc);
repository.emitter.emit( repository.emitter.emit(
@ -108,7 +115,7 @@ export abstract class BaseRepository<
); );
}); });
hooks?.validate.post.execute(async function (created: HydratedDocument<T>) { hooks.validate.post.execute(async function (created: HydratedDocument<T>) {
await repository.postCreateValidate(created); await repository.postCreateValidate(created);
repository.emitter.emit( repository.emitter.emit(
repository.getEventName(EHook.postCreateValidate), repository.getEventName(EHook.postCreateValidate),
@ -116,13 +123,13 @@ export abstract class BaseRepository<
); );
}); });
hooks?.save.pre.execute(async function () { hooks.save.pre.execute(async function () {
const doc = this as HydratedDocument<T>; const doc = this as HydratedDocument<T>;
await repository.preCreate(doc); await repository.preCreate(doc);
repository.emitter.emit(repository.getEventName(EHook.preCreate), doc); repository.emitter.emit(repository.getEventName(EHook.preCreate), doc);
}); });
hooks?.save.post.execute(async function (created: HydratedDocument<T>) { hooks.save.post.execute(async function (created: HydratedDocument<T>) {
await repository.postCreate(created); await repository.postCreate(created);
repository.emitter.emit( repository.emitter.emit(
repository.getEventName(EHook.postCreate), repository.getEventName(EHook.postCreate),
@ -130,7 +137,7 @@ export abstract class BaseRepository<
); );
}); });
hooks?.deleteOne.pre.execute(async function () { hooks.deleteOne.pre.execute(async function () {
const query = this as Query<DeleteResult, D, unknown, T, 'deleteOne'>; const query = this as Query<DeleteResult, D, unknown, T, 'deleteOne'>;
const criteria = query.getQuery(); const criteria = query.getQuery();
await repository.preDelete(query, criteria); await repository.preDelete(query, criteria);
@ -151,13 +158,13 @@ export abstract class BaseRepository<
); );
}); });
hooks?.deleteMany.pre.execute(async function () { hooks.deleteMany.pre.execute(async function () {
const query = this as Query<DeleteResult, D, unknown, T, 'deleteMany'>; const query = this as Query<DeleteResult, D, unknown, T, 'deleteMany'>;
const criteria = query.getQuery(); const criteria = query.getQuery();
await repository.preDelete(query, criteria); await repository.preDelete(query, criteria);
}); });
hooks?.deleteMany.post.execute(async function (result: DeleteResult) { hooks.deleteMany.post.execute(async function (result: DeleteResult) {
repository.emitter.emit( repository.emitter.emit(
repository.getEventName(EHook.postDelete), repository.getEventName(EHook.postDelete),
result, result,
@ -166,11 +173,13 @@ export abstract class BaseRepository<
await repository.postDelete(query, result); await repository.postDelete(query, result);
}); });
hooks?.findOneAndUpdate.pre.execute(async function () { hooks.findOneAndUpdate.pre.execute(async function () {
const query = this as Query<D, D, unknown, T, 'findOneAndUpdate'>; const query = this as Query<D, D, unknown, T, 'findOneAndUpdate'>;
const criteria = query.getFilter(); const criteria = query.getFilter();
const updates = query.getUpdate(); const updates = query.getUpdate();
if (!updates) {
throw new Error('Unable to run findOneAndUpdate pre hook');
}
await repository.preUpdate(query, criteria, updates); await repository.preUpdate(query, criteria, updates);
repository.emitter.emit( repository.emitter.emit(
repository.getEventName(EHook.preUpdate), repository.getEventName(EHook.preUpdate),
@ -179,11 +188,13 @@ export abstract class BaseRepository<
); );
}); });
hooks?.updateMany.pre.execute(async function () { hooks.updateMany.pre.execute(async function () {
const query = this as Query<D, D, unknown, T, 'updateMany'>; const query = this as Query<D, D, unknown, T, 'updateMany'>;
const criteria = query.getFilter(); const criteria = query.getFilter();
const updates = query.getUpdate(); const updates = query.getUpdate();
if (!updates) {
throw new Error('Unable to execute updateMany() pre-hook');
}
await repository.preUpdateMany(query, criteria, updates); await repository.preUpdateMany(query, criteria, updates);
repository.emitter.emit( repository.emitter.emit(
repository.getEventName(EHook.preUpdateMany), repository.getEventName(EHook.preUpdateMany),
@ -192,7 +203,7 @@ export abstract class BaseRepository<
); );
}); });
hooks?.updateMany.post.execute(async function (updated: any) { hooks.updateMany.post.execute(async function (updated: any) {
const query = this as Query<D, D, unknown, T, 'updateMany'>; const query = this as Query<D, D, unknown, T, 'updateMany'>;
await repository.postUpdateMany(query, updated); await repository.postUpdateMany(query, updated);
repository.emitter.emit( repository.emitter.emit(
@ -201,7 +212,7 @@ export abstract class BaseRepository<
); );
}); });
hooks?.findOneAndUpdate.post.execute(async function ( hooks.findOneAndUpdate.post.execute(async function (
updated: HydratedDocument<T>, updated: HydratedDocument<T>,
) { ) {
if (updated) { if (updated) {
@ -227,10 +238,10 @@ export abstract class BaseRepository<
} }
protected async executeOne<R extends Omit<T, P>>( protected async executeOne<R extends Omit<T, P>>(
query: Query<T, T>, query: Query<T | null, T>,
cls: new () => R, cls: new () => R,
options?: ClassTransformOptions, options?: ClassTransformOptions,
): Promise<R> { ): Promise<R | null> {
const doc = await query.lean(this.leanOpts).exec(); const doc = await query.lean(this.leanOpts).exec();
return plainToClass(cls, doc, options ?? this.transformOpts); return plainToClass(cls, doc, options ?? this.transformOpts);
} }
@ -238,15 +249,15 @@ export abstract class BaseRepository<
protected findOneQuery( protected findOneQuery(
criteria: string | TFilterQuery<T>, criteria: string | TFilterQuery<T>,
projection?: ProjectionType<T>, projection?: ProjectionType<T>,
): Query<T, T, object, T, 'findOne', object> { ): Query<T | null, T, object, T, 'findOne', object> {
if (!criteria) { if (!criteria) {
// An empty criteria would return the first document that it finds // An empty criteria would return the first document that it finds
throw new Error('findOneQuery() should not have an empty criteria'); throw new Error('findOneQuery() should not have an empty criteria');
} }
return typeof criteria === 'string' return typeof criteria === 'string'
? this.model.findById(criteria, projection) ? this.model.findById<HydratedDocument<T>>(criteria, projection)
: this.model.findOne<T>(criteria, projection); : this.model.findOne<HydratedDocument<T>>(criteria, projection);
} }
async findOne( async findOne(
@ -256,7 +267,7 @@ export abstract class BaseRepository<
) { ) {
if (!criteria) { if (!criteria) {
// @TODO : Issue a warning ? // @TODO : Issue a warning ?
return Promise.resolve(undefined); return Promise.resolve(null);
} }
const query = this.findOneQuery(criteria, projection); const query = this.findOneQuery(criteria, projection);
@ -266,12 +277,12 @@ export abstract class BaseRepository<
async findOneAndPopulate( async findOneAndPopulate(
criteria: string | TFilterQuery<T>, criteria: string | TFilterQuery<T>,
projection?: ProjectionType<T>, projection?: ProjectionType<T>,
): Promise<TFull> { ): Promise<TFull | null> {
this.ensureCanPopulate(); this.ensureCanPopulate();
const query = this.findOneQuery(criteria, projection).populate( const query = this.findOneQuery(criteria, projection).populate(
this.populate, this.populate,
); );
return await this.executeOne(query, this.clsPopulate); return await this.executeOne(query, this.clsPopulate!);
} }
protected findQuery( protected findQuery(
@ -371,13 +382,13 @@ export abstract class BaseRepository<
const query = this.findQuery(filters, pageQuery, projection).populate( const query = this.findQuery(filters, pageQuery, projection).populate(
this.populate, this.populate,
); );
return await this.execute(query, this.clsPopulate); return await this.execute(query, this.clsPopulate!);
} }
const query = this.findQuery(filters, pageQuery, projection).populate( const query = this.findQuery(filters, pageQuery, projection).populate(
this.populate, this.populate,
); );
return await this.execute(query, this.clsPopulate); return await this.execute(query, this.clsPopulate!);
} }
protected findAllQuery( protected findAllQuery(
@ -393,7 +404,7 @@ export abstract class BaseRepository<
async findAllAndPopulate(sort?: QuerySortDto<T>): Promise<TFull[]> { async findAllAndPopulate(sort?: QuerySortDto<T>): Promise<TFull[]> {
this.ensureCanPopulate(); this.ensureCanPopulate();
const query = this.findAllQuery(sort).populate(this.populate); const query = this.findAllQuery(sort).populate(this.populate);
return await this.execute(query, this.clsPopulate); return await this.execute(query, this.clsPopulate!);
} }
/** /**
@ -431,7 +442,7 @@ export abstract class BaseRepository<
const query = this.findPageQuery(filters, pageQuery).populate( const query = this.findPageQuery(filters, pageQuery).populate(
this.populate, this.populate,
); );
return await this.execute(query, this.clsPopulate); return await this.execute(query, this.clsPopulate!);
} }
async countAll(): Promise<number> { async countAll(): Promise<number> {
@ -463,7 +474,7 @@ export abstract class BaseRepository<
async updateOne<D extends Partial<U>>( async updateOne<D extends Partial<U>>(
criteria: string | TFilterQuery<T>, criteria: string | TFilterQuery<T>,
dto: UpdateQuery<D>, dto: UpdateQuery<D>,
): Promise<T> { ): Promise<T | null> {
const query = this.model.findOneAndUpdate<T>( const query = this.model.findOneAndUpdate<T>(
{ {
...(typeof criteria === 'string' ? { _id: criteria } : criteria), ...(typeof criteria === 'string' ? { _id: criteria } : criteria),
@ -478,6 +489,10 @@ export abstract class BaseRepository<
const filterCriteria = query.getFilter(); const filterCriteria = query.getFilter();
const queryUpdates = query.getUpdate(); const queryUpdates = query.getUpdate();
if (!queryUpdates) {
throw new Error('Unable to execute updateOne() - No updates');
}
await this.preUpdateValidate(filterCriteria, queryUpdates); await this.preUpdateValidate(filterCriteria, queryUpdates);
this.emitter.emit( this.emitter.emit(
this.getEventName(EHook.preUpdateValidate), this.getEventName(EHook.preUpdateValidate),
@ -491,7 +506,6 @@ export abstract class BaseRepository<
filterCriteria, filterCriteria,
queryUpdates, queryUpdates,
); );
return await this.executeOne(query, this.cls); return await this.executeOne(query, this.cls);
} }

View File

@ -33,7 +33,7 @@ export abstract class BaseService<
criteria: string | TFilterQuery<T>, criteria: string | TFilterQuery<T>,
options?: ClassTransformOptions, options?: ClassTransformOptions,
projection?: ProjectionType<T>, projection?: ProjectionType<T>,
): Promise<T> { ): Promise<T | null> {
return await this.repository.findOne(criteria, options, projection); return await this.repository.findOne(criteria, options, projection);
} }
@ -173,7 +173,7 @@ export abstract class BaseService<
async updateOne<D extends Partial<Omit<T, keyof BaseSchema>>>( async updateOne<D extends Partial<Omit<T, keyof BaseSchema>>>(
criteria: string | TFilterQuery<T>, criteria: string | TFilterQuery<T>,
dto: D, dto: D,
): Promise<T> { ): Promise<T | null> {
return await this.repository.updateOne(criteria, dto); return await this.repository.updateOne(criteria, dto);
} }

View File

@ -25,7 +25,7 @@ type PostHook = (...args: any[]) => void;
interface LifecycleHook { interface LifecycleHook {
pre: PreHook & { execute: (newCallback: PreHook) => void }; pre: PreHook & { execute: (newCallback: PreHook) => void };
post?: PostHook & { execute: (newCallback: PostHook) => void }; post: PostHook & { execute: (newCallback: PostHook) => void };
} }
type LifecycleHooks = { type LifecycleHooks = {
@ -77,12 +77,9 @@ export class LifecycleHookManager {
for (const [op, types] of Object.entries(operations)) { for (const [op, types] of Object.entries(operations)) {
const hooks: LifecycleHook = { const hooks: LifecycleHook = {
pre: this.createLifecycleCallback() as LifecycleHook['pre'], pre: this.createLifecycleCallback() as LifecycleHook['pre'],
post: this.createLifecycleCallback() as LifecycleHook['post'],
}; };
if (types.includes('post')) {
hooks.post = this.createLifecycleCallback() as LifecycleHook['post'];
}
types.forEach((type) => { types.forEach((type) => {
schema[type](op, hooks[type]); schema[type](op, hooks[type]);
}); });