Skip to content

Instantly share code, notes, and snippets.

@devhammed
Last active August 19, 2025 18:38
Show Gist options
  • Select an option

  • Save devhammed/4e894df9e9766c2560e507fee4bb1ad1 to your computer and use it in GitHub Desktop.

Select an option

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