Today I finalized tenant-aware access across all core modules using a shared ScopedRepository<T> abstraction. This was the largest refactoring effort so far: 12 modules, 80+ unit tests, and one generic strategy to guarantee data isolation via subscriber_id.
Motivation
The goal was to ensure that each request only sees data belonging to its tenant (subscriber_id). Instead of duplicating this logic across repositories, I built a dedicated class: ScopedRepository<T>.
The ScopedRepository<T> abstraction
This generic class wraps a regular TypeORM Repository<T> and uses the current user’s scope to transparently inject subscriber_id into queries.
Key methods:
find(options)→ injectssubscriber_idintowherefindOne(options)→ injectssubscriber_idintowheresave(entity)→ verifies entity’ssubscriberIdbefore savingremove(entity)→ same
Example:
const roles = await this.scopedRepo.find({
relations: ['users'],
});
This expands to:
repo.find({
relations: ['users'],
where: { subscriber_id: '...' },
});
All without manually accessing subscriberId in each service method.
The CurrentUserService
This request-scoped service provides per-request context:
@Injectable({ scope: Scope.REQUEST })
export class CurrentUserService {
constructor(@Inject(REQUEST) private readonly request: Request) {}
get subscriberId(): string | null {
const user = this.request.user as any;
if (!user) return null;
if (user.role === 'admin') return null;
return user.subscriber_id ?? null;
}
get isScoped(): boolean {
return this.subscriberId !== null;
}
}
This is used inside ScopedRepository:
private scoped<U extends FindManyOptions<T> | FindOneOptions<T>>(options: U): U {
if (!this.currentUser.isScoped) return options;
return {
...options,
where: {
...(options.where ?? {}),
subscriber_id: this.currentUser.subscriberId,
},
};
}
Unit Tests
Each of the 12 modules includes unit tests verifying the use of scoped access.
What is tested?
findAllalways includeswhere.subscriber_idfindOneuses{ id, subscriber_id }inwheresaveoperations assign correctsubscriberIdremoveonly affects scoped entities- Whether queries include the correct `subscriber_id`
- Whether saved and removed entities match the scoped tenant
- That unscoped operations (e.g., admin) bypass the filter when needed
Modules covered
- Group
- GroupMembership
- GroupRelation
- User
- UserContract
- ContractType
- Address
- Customer
- Role
- Payment
- Subscription
Every repository interaction is covered.
Example: RoleService
describe('findOne', () => {
it('should query with subscriber_id', async () => {
repo.findOne.mockResolvedValue(mockRole);
const result = await service.findOne('123');
expect(repo.findOne).toHaveBeenCalledWith({
where: { id: '123', subscriber_id: 'test-subscriber-id' },
relations: ['users'],
});
expect(result).toEqual(mockRole);
});
});
Example: SubscriptionService
describe('findAll', () => {
it('should apply tenant scope with relations', async () => {
await service.findAll();
expect(repo.find).toHaveBeenCalledWith({
where: { subscriber_id: 'test-subscriber-id' },
relations: ['user', 'plan', 'payments'],
});
});
});
Example: ScopedRepository save()
describe('save', () => {
it('should throw if subscriberId is missing on entity', async () => {
await expect(
scopedRepo.save({ name: 'X' } as any),
).rejects.toThrow('Missing subscriberId on entity');
});
it('should proceed if entity has subscriberId', async () => {
const entity = { name: 'X', subscriberId: 'sub-123' };
repo.save.mockResolvedValue(entity);
const result = await scopedRepo.save(entity);
expect(result).toEqual(entity);
});
});
Workload
- 12 modules adjusted
- 80+ test cases updated or added
- ScopedRepository and CurrentUserService created
- One long day (much too long)
release: v0.6.0

Dodaj komentarz