npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

typeorm-scoped-repository

v0.3.0

Published

TypeORM ScopedRepository with fortress pattern — automatic multi-scope isolation (organisation, user, agent, tenant) on every query. Framework-agnostic.

Readme

typeorm-scoped-repository

TypeORM ScopedRepository with fortress pattern for NestJS — automatic multi-scope isolation on every query. No raw SQL, no forgotten WHERE clauses, no cross-tenant data leaks.

Features

  • Automatic scope injection — every find, findOne, save, update, delete gets the scope fields added as WHERE conditions
  • Fortress pattern.where() is silently converted to .andWhere() on query builders, so scope filters cannot be accidentally cleared
  • Composite scopes — scope on any combination of fields (organisationId, userId + agentId, accountId + ownerSpace, etc.)
  • Layered scoping — add scope fields progressively with withScope()
  • Transaction supportwithTransaction(manager) maintains scope inside transactions
  • TypeScript-first, zero magic

Install

npm install typeorm-scoped-repository

Usage

Single scope (organisation isolation)

import { ScopedRepository } from 'typeorm-scoped-repository';

@Injectable()
export class ArticleService {
  constructor(
    @InjectRepository(ArticleEntity)
    private readonly _repo: Repository<ArticleEntity>,
  ) {}

  private repo(organisationId: string) {
    return new ScopedRepository(this._repo, { organisationId });
  }

  async findAll(organisationId: string) {
    return this.repo(organisationId).find();
    // Executes: SELECT * FROM articles WHERE organisation_id = $1
  }

  async create(organisationId: string, data: Partial<ArticleEntity>) {
    return this.repo(organisationId).save(data as ArticleEntity);
    // Always stamps organisationId — cannot forget it
  }
}

Composite scope (multi-field isolation)

const agentSpace = md5(`${userId}:${agentId}`).slice(0, 12);

const repo = new ScopedRepository(contextVectorRepo, {
  accountId: 'default',
  ownerSpace: agentSpace,
});

// All queries: WHERE account_id = 'default' AND owner_space = 'a3f9c2b1e8d4'
const records = await repo.find();

Layered scoping

// Base scope — account level
const accountRepo = new ScopedRepository(repo, { accountId: 'acme' });

// Add agent scope on top
const agentRepo = accountRepo.withScope({ ownerSpace: '10f5d88f294c' });

// agentRepo: WHERE account_id = 'acme' AND owner_space = '10f5d88f294c'
// accountRepo unchanged: WHERE account_id = 'acme'

Fortress pattern (query builder)

const qb = repo.createQueryBuilder('entity');
// Scope already applied: WHERE entity.organisation_id = 'org-123'

qb.where('entity.name = :name', { name: 'test' });
// Safe! .where() is converted to .andWhere() — scope NOT cleared
// Final: WHERE entity.organisation_id = 'org-123' AND entity.name = 'test'

Idempotent insert (ON CONFLICT DO NOTHING)

save() uses TypeORM's upsert-style Repository.save() — a retried call updates the existing row. When you need true idempotency (retried call is a no-op, never an update), use insert() with orIgnore:

await repo.insert(entity, { orIgnore: true });
// PostgreSQL: INSERT … ON CONFLICT DO NOTHING
// SQLite:     INSERT OR IGNORE …

Scope is stamped into the VALUES payload, so a retried worker cannot accidentally land the row in a different scope. Bulk inserts (insert([e1, e2, ...])) stamp scope on every row.

Transactions

await dataSource.transaction(async (manager) => {
  const txRepo = myRepo.withTransaction(manager);
  // Same scope, within the transaction
  await txRepo.save(entity);
});

withTransaction(manager, EntityClass?) returns a fresh scoped repo bound to the transaction's EntityManager. The fortress is preserved across the boundary: queries built off this repo (find, update, insert, createQueryBuilder('alias').setLock(...), …) still auto-inject scope.

Pass the entity class as the second argument if the underlying repo's target cannot be inferred (typically only happens in tests with hand-rolled mocks).

Recommended pattern (NestJS)

The shape below is the one this package was designed for. Consumer codebases that have followed it have stayed bypass-free; codebases that diverged from it have rediscovered every fortress hole the package was built to close.

// 1. Feature module — register entities through the package's helper
@Module({
  imports: [ScopedRepositoryModule.forFeature([ArticleEntity, CategoryEntity])],
  providers: [ArticleService],
})
export class ArticleModule {}
// 2. Repository class — inject the FACTORY, never @InjectRepository
@Injectable()
export class ArticleRepository {
  constructor(
    @InjectScopedFactory(ArticleEntity)
    private readonly articles: ScopedRepositoryFactory<ArticleEntity>,
  ) {}

  // Methods take (scope, …args, em?: EntityManager) so they compose with
  // caller-owned transactions. Never store scope in the class.
  async findById(scope: Scope, id: string) {
    return this.articles(scope).findOne({ where: { id } });
  }

  async insertOrIgnore(scope: Scope, article: Article, em: EntityManager) {
    await this.articles(scope)
      .withTransaction(em, ArticleEntity)
      .insert(article, { orIgnore: true });
  }

  async findForUpdate(scope: Scope, ids: string[], em: EntityManager) {
    return this.articles(scope)
      .withTransaction(em, ArticleEntity)
      .createQueryBuilder('a')
      .where('a.id = ANY(:ids)', { ids: [...ids].sort() })
      .setLock('pessimistic_write')
      .getMany();
  }
}
// 3. Service / use-case — OWNS the transaction boundary
@Injectable()
export class ArchiveService {
  constructor(
    private readonly dataSource: DataSource,
    private readonly articleRepo: ArticleRepository,
    private readonly categoryRepo: CategoryRepository,
  ) {}

  async archiveCategory(scope: Scope, categoryId: string) {
    return this.dataSource.transaction(async (em) => {
      const locked = await this.articleRepo.findForUpdate(scope, [categoryId], em);
      await this.categoryRepo.updateStatus(scope, categoryId, 'archived', em);
      // Both repos participate in the same transaction with scope intact.
    });
  }
}

Anti-patterns checklist

Treat each of these as a code-review red flag.

| ❌ Anti-pattern | ✅ Correct shape | |---|---| | @InjectRepository(Entity) for a scoped entity (factory and shadow injected together) | @InjectScopedFactory(Entity) only | | Manual scope stamping: (entity as X).orgId = orgId before insert | Let scopedRepo.insert(entity) / .save(entity) stamp scope automatically | | Raw em.createQueryBuilder(Entity, 'alias') inside a caller-owned transaction | scopedRepo.withTransaction(em, Entity).createQueryBuilder('alias') | | get manager(): EntityManager { return this.repo.manager; } exposed publicly | Use scopedRepo.transaction(cb) for self-opened transactions | | this.repo.manager.transaction(...) to open a transaction inside a repository | scopedRepo.transaction(async (txRepo, em) => …) | | Service-owned scope state: class FooService { constructor() { this.repo = … } } storing scope in the class | Pass scope through method signatures; never cache it in a class | | INSERT … ON CONFLICT DO NOTHING written by hand as a QueryBuilder chain | scopedRepo.insert(entity, { orIgnore: true }) | | repo.where(…) rewritten to add an explicit scope clause | Trust the fortress: .where(…) is silently converted to .andWhere(…) so scope cannot be cleared |

When ReflectiveAI-shaped exceptions are legitimate

These patterns look like anti-patterns but are sometimes structurally required:

  • Raw SQL via dataSource.query(sql, params) for recursive CTEs. The fortress is a QueryBuilder wrapper; raw SQL is outside it by design. Bind scope as a numbered parameter ($3) at every WHERE clause that touches a scoped table. Document the carve-out at the top of the method.
  • Multi-transaction workflows where one transaction's writes must commit before another's reads. The service still owns each transaction boundary; just re-enter the fortress (withTransaction(em)) inside every block. The two transactions stay separate; the bypasses don't.

See docs/nestjs.md for the full discussion and worked examples.

API

new ScopedRepository<T>(repo, scope)

| Parameter | Type | Description | |---|---|---| | repo | Repository<T> | TypeORM repository | | scope | Scope | Record<string, string> of field names to values |

Methods

| Method | Description | |---|---| | find(options?) | Find all matching records (scope always applied) | | findOne(options) | Find one record (scope always applied) | | save(entity) | Save with scope fields stamped (upsert-style) | | insert(entity, { orIgnore? }) | Plain INSERT with scope stamped; orIgnore for INSERT … ON CONFLICT DO NOTHING | | create(data) | Create entity instance with scope fields | | update(id, partial) | Update by id within scope | | delete(id) | Delete by id within scope | | count(options?) | Count within scope | | createQueryBuilder(alias) | Fortress-wrapped query builder | | withScope(additionalScope) | Returns new repo with extended scope | | withTransaction(manager, entityClass?) | Returns new repo bound to an external transaction manager | | transaction(cb) | Opens a new transaction; callback receives (scopedRepo, em) | | getScope() | Returns a readonly copy of the current scope |

Motivation

This pattern was extracted from a production NestJS application with strict organisation-level data isolation. It proved robust enough to generalise for any multi-scope use case.

License

MIT