Last active
August 19, 2025 18:38
-
-
Save devhammed/4e894df9e9766c2560e507fee4bb1ad1 to your computer and use it in GitHub Desktop.
Hide TypeORM Entities Marked As Inactive From All Queries With Support For Nested Relations (except for admin users that can specify to see them with active records by setting "includeInactive" to true or only them by setting "onlyInactive" to true)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // This must be in a top-level module as a provider (e.g. AppModule or a shared module marked as Global) | |
| import { Inject, Injectable, Scope } from '@nestjs/common'; | |
| import { REQUEST } from '@nestjs/core'; | |
| import { Request } from 'express'; | |
| import { DataSource, EntityTarget, ObjectLiteral } from 'typeorm'; | |
| import { ActiveScopeRepository } from '@/common/repositories/active-scope.repository'; | |
| @Injectable({ | |
| scope: Scope.REQUEST, | |
| }) | |
| export class ActiveScopeRepositoryFactory { | |
| constructor( | |
| @Inject(REQUEST) private readonly request: Request, | |
| private readonly dataSource: DataSource, | |
| ) {} | |
| getRepository<Entity extends ObjectLiteral>(entity: EntityTarget<Entity>) { | |
| const manager = this.dataSource.manager; | |
| const queryRunner = this.dataSource.createQueryRunner(); | |
| const role = this.request.user?.role; | |
| return new ActiveScopeRepository(entity, manager, queryRunner, role); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { | |
| EntityManager, | |
| EntityTarget, | |
| FindManyOptions, | |
| FindOneOptions, | |
| FindOptionsWhere, | |
| ObjectLiteral, | |
| QueryRunner, | |
| Repository, | |
| } from 'typeorm'; | |
| import { User } from '@/user/entities/user.entity'; | |
| import { UserRole } from '@/user/enums/user-role.enum'; | |
| import { VisibilityOptions } from '@/common/interfaces/visibility-options.interface'; | |
| export class ActiveScopeRepository< | |
| Entity extends ObjectLiteral, | |
| > extends Repository<Entity> { | |
| constructor( | |
| target: EntityTarget<Entity>, | |
| manager: EntityManager, | |
| queryRunner?: QueryRunner, | |
| private readonly userRole?: User['role'], | |
| ) { | |
| super(target, manager, queryRunner); | |
| } | |
| async find( | |
| options: FindManyOptions<Entity>, | |
| visibilityOptions: VisibilityOptions = {}, | |
| ) { | |
| const where = options.where ? options.where : {}; | |
| this.applyVisibilityToEntity(where, visibilityOptions); | |
| this.applyVisibilityToRelations(where, visibilityOptions); | |
| return super.find({ | |
| ...options, | |
| where, | |
| }); | |
| } | |
| async findOne( | |
| options: FindOneOptions<Entity>, | |
| visibilityOptions: VisibilityOptions = {}, | |
| ) { | |
| const where = options.where ? options.where : {}; | |
| this.applyVisibilityToEntity(where, visibilityOptions); | |
| this.applyVisibilityToRelations(where, visibilityOptions); | |
| return super.findOne({ | |
| ...options, | |
| where, | |
| }); | |
| } | |
| async findAndCount( | |
| options: FindManyOptions<Entity>, | |
| visibilityOptions: VisibilityOptions = {}, | |
| ) { | |
| const where = options.where ? options.where : {}; | |
| this.applyVisibilityToEntity(where, visibilityOptions); | |
| this.applyVisibilityToRelations(where, visibilityOptions); | |
| return super.findAndCount({ | |
| ...options, | |
| where, | |
| }); | |
| } | |
| private applyVisibilityToEntity( | |
| obj: never[] | Record<string, unknown>, | |
| visibilityOptions: VisibilityOptions = {}, | |
| ): never[] | Record<string, unknown> { | |
| if (Array.isArray(obj)) { | |
| if (obj.length === 0) { | |
| obj.push({} as never); | |
| } | |
| return obj.map<never>( | |
| (e) => this.applyVisibilityToEntity(e, visibilityOptions) as never, | |
| ); | |
| } | |
| if (this.userRole === UserRole.ADMIN) { | |
| if (visibilityOptions.onlyInactive) obj.active = false; | |
| else if (!visibilityOptions.withInactive) obj.active = true; | |
| } else { | |
| obj.active = true; | |
| } | |
| return obj; | |
| } | |
| private applyVisibilityToRelations( | |
| obj: never[] | Record<string, unknown>, | |
| visibilityOptions: VisibilityOptions = {}, | |
| ): void { | |
| if (visibilityOptions.scopedRelations) { | |
| for (const path of visibilityOptions.scopedRelations) { | |
| const parts = path.split('.'); | |
| const applyToPath = (cursor: FindOptionsWhere<Entity>) => { | |
| if (Array.isArray(cursor)) { | |
| cursor.forEach(applyToPath); | |
| return; | |
| } | |
| let c = cursor as Record<string, unknown>; | |
| for (const part of parts) { | |
| c[part] = c[part] || {}; | |
| c = c[part] as Record<string, unknown>; | |
| } | |
| this.applyVisibilityToEntity(c, visibilityOptions); | |
| }; | |
| applyToPath(obj as FindOptionsWhere<Entity>); | |
| } | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Injectable, NotFoundException } from '@nestjs/common'; | |
| import { Test } from '@/test/entities/test.entity'; | |
| @Injectable() | |
| export class TestService { | |
| constructor( | |
| private readonly activeScopeRepositoryFactory: ActiveScopeRepositoryFactory, | |
| ) {} | |
| get testRepository() { | |
| return this.activeScopeRepositoryFactory.getRepository(Test); | |
| } | |
| async getTest(id: Currency['id']): Promise<Test> { | |
| const test = await this.testRepository.findOne( | |
| { | |
| where: { | |
| id, | |
| }, | |
| relations: { | |
| user: true, | |
| }, | |
| }, | |
| { | |
| withInactive: true, | |
| }, | |
| ); | |
| if (!test) { | |
| throw new NotFoundException('Test not found.'); | |
| } | |
| return test; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export interface VisibilityOptions { | |
| onlyInactive?: boolean; | |
| withInactive?: boolean; | |
| scopedRelations?: string[]; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment