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

@deorta-dev/nestjs-repository-core

v0.1.1

Published

Genera repositorios genéricos para NestJS + Mongoose por entidad (sin crear una clase por entidad), con caché read-through con TTL y backups de solo-escritura resilientes a desconexiones.

Readme

Language: English · Español

@deorta-dev/nestjs-repository-core

A NestJS + Mongoose library that generates a generic repository service (BaseRepositoryService<T>) for any entity — with read-through caching and write-only backup replicas — without having to write an XxxOrmService / XxxOrmModule class per entity.

Replaces the per-entity PositionOrmService + PositionOrmModule pattern with a single RepositoryOrmModule.register(...) call.

Built and verified with tsc --strict against @nestjs/common, @nestjs/mongoose, mongoose and class-transformer.

Installation

npm install @deorta-dev/nestjs-repository-core

You also need these installed (they're peerDependencies, not installed automatically):

npm install @nestjs/common @nestjs/mongoose mongoose class-transformer reflect-metadata rxjs

Basic usage

import { RepositoryOrmModule, RepositoryInject, IBaseRepositoryService } from '@deorta-dev/nestjs-repository-core';

// position-repository.module.ts
export const PositionRepositoryModule = RepositoryOrmModule.register({
  entity: Position,
  schema: positionSchema,
  connectionName: ConnectionNames.OPERATION_MDB,
});

// in any @Module:
@Module({ imports: [PositionRepositoryModule] })
export class SomeModule {}

// in any service: inject against the INTERFACE, not the concrete class
// (so swapping in a customService later doesn't require touching this).
constructor(
  @RepositoryInject(PositionRepositoryModule)
  private readonly positionRepository: IBaseRepositoryService<Position>,
) {}

The injection token is always ${Entity.name}RepositoryService (e.g. PositionRepositoryService), so it doesn't matter how many times you call .register() for the same entity — the token is consistent, and @RepositoryInject(whateverRegisterReturned) always resolves to the same provider.

See src/examples/position-repository.example.ts for a complete migration example using Position.

BaseRepositoryService<T> API (the IBaseRepositoryService<T> contract)

| Method | What it does | |---|---| | findOne(filter, opts?) | Cache-first by default; falls back to main on a miss. opts.target = 'main' \| 'cache' forces a specific connection (no fallback). | | find(filter, opts?) | Same as findOne but returns a list. Supports opts.sort/limit/skip/projection. | | create(dto) | Inserts into main; the resulting document (with its _id) is replicated to cache and all backups. | | insertMany(dtos[]) | Same as create, in bulk (insertMany + bulkWrite against cache/backups). | | updateOne(filter, update) | Updates main first, then replicates the resulting document to cache and backups. | | updateMany(filter, update) | Same, in bulk. | | deleteOne(filter) / deleteMany(filter) | Deletes from main first, then from cache and backups, and records a "tombstone" so periodic sync knows about it too. |

Resilience: what happens if cache or backups are down?

As long as main is up, the service works — regardless of the state of cache or any backup connection.

  • Reads (findOne/find): if the cache connection isn't ready or the query fails, it's treated as a cache miss and main is queried directly — the error never propagates.
  • Writes (create/insertMany/updateOne/updateMany/deleteOne/deleteMany): always run against main first. Propagation to cache and each backup is attempted immediately; if a connection isn't ready (readyState !== 1) or the operation fails, that write is queued in memory (one queue per secondary connection) instead of failing the whole operation.
  • The pending-ops queue retries itself:
    • Every pendingOps.retryIntervalMs (default 5000 ms).
    • As soon as the connection emits mongoose's connected event (immediate reaction, no need to wait for the next tick).
    • If more than pendingOps.maxQueueSize operations pile up (default 1000) because a connection has been down for a while, the oldest ones are dropped to avoid unbounded memory growth — that's fine, because BackupSyncService catches backups up against main anyway, and for cache, the next find/findOne simply repopulates it.
  • Queued operations are always _id-based upserts/deletes (idempotent), so retrying them in order, even multiple times, is safe.
  • Each backup is independent: if you have two backups and one is down, the other keeps advancing with its own checkpoint; the one that was down catches up on its own once it's back (there's no shared checkpoint that one problematic connection can block).
  • Tombstones (used to propagate deletes to backups) are only purged from the collection once every configured backup has already applied them — so a backup that was down doesn't lose the information it needs to catch up.
RepositoryOrmModule.register({
  // ...
  pendingOps: {
    retryIntervalMs: 5000, // how often pending writes are retried
    maxQueueSize: 1000,    // in-memory cap per secondary connection
  },
});

Custom service (customService)

By default, register(...) uses BaseRepositoryService. If you need different behavior for a particular entity, you can pass your own class via customService:

RepositoryOrmModule.register({
  entity: Position,
  schema: positionSchema,
  connectionName: ConnectionNames.OPERATION_MDB,
  customService: PositionRepositoryService, // your class
});

customService is typed as Type<IBaseRepositoryService<T>>, so TypeScript won't let you assign a class that doesn't satisfy the interface (findOne, find, create, insertMany, updateOne, updateMany, deleteOne, deleteMany, matching the exact IBaseRepositoryService<T> signatures).

Two ways to write one (both shown in src/examples/custom-repository-service.example.ts):

  1. Extend BaseRepositoryService<T> (recommended): you inherit all the cache/backup resilience and only override the method(s) you care about, calling super.method(...) if you want to keep the original behavior.

    class PositionRepositoryService extends BaseRepositoryService<Position> {
      async create(dto: Partial<Position>) {
        const created = await super.create(dto);
        console.log('Position created:', created);
        return created;
      }
    }
  2. Implement IBaseRepositoryService<T> from scratch: useful if you want a completely different strategy (e.g. skip cache/backups entirely). Its constructor must accept the same 9 parameters that register(...) already resolves for you: entity, options, mainModel, cacheModel, cacheConfig, backupModels, backupLabels, tombstoneModel, pendingOpsConfig (even if you don't use all of them).

Either way, you inject your custom service exactly like the default one, with @RepositoryInject(...) — nothing else in your code changes, because both satisfy IBaseRepositoryService<T>.

RepositoryOrmModule.register(...) configuration reference

{
  entity: Position,            // entity class
  schema: positionSchema,      // mongoose schema
  connectionName: '...',       // main connection
  options: {},                 // your BaseOrmOptions

  cache: {                     // OPTIONAL
    connectionName: '...',
    ttlSeconds: 300,           // how long a document lives in the cache connection
  },

  backups: [                   // OPTIONAL, array of write-only connections
    { connectionName: '...' },
    { connectionName: '...' },
  ],

  backupSync: {                // OPTIONAL, only applies if `backups` is set
    enabled: true,              // if false, nothing syncs automatically
    intervalMs: 60_000,         // how often main vs. backups is checked
    runOnStart: true,           // run a check as soon as the module starts
    batchSize: 500,             // documents per batch per check
  },

  pendingOps: {                // OPTIONAL
    retryIntervalMs: 5000,
    maxQueueSize: 1000,
  },

  customService: PositionRepositoryService, // OPTIONAL, default: BaseRepositoryService
}

Design notes

A few implementation choices worth knowing about if you're extending this library:

  1. Cache vs. main on reads: findOne/find without an explicit target query cache first and fall back to main on a miss (and repopulate cache in the background, without blocking the response). With target: 'main' or target: 'cache', only that connection is queried, with no fallback.

  2. Cache TTL: uses an "expire at a specific time" TTL index (expireAfterSeconds: 0 on a _cacheExpiresAt field) instead of classic Mongo TTL, so every write can set its own expiration based on cache.ttlSeconds, independent of when the index was created.

  3. How backup catch-up is detected: for inserts/updates, updatedTime is compared against a per-entity, per-backup checkpoint (this assumes your model keeps updatedTime current on every write). For deletes, instead of diffing the entire _id set (expensive at scale), deleteOne/deleteMany record a "tombstone" (_id + deletion time) on the main connection, which the sync process consumes and clears.

    If your "deletes" are actually soft-deletes (a trashed: true flag), the tombstone mechanism simply goes unused — updatedTime-based sync already covers it, since flipping trashed also bumps updatedTime.

  4. Triggering backup sync: by default, if backupSync.enabled is true, the service starts its own internal setInterval (onModuleInit/onModuleDestroy). If you'd rather have a lightweight external process decide when to sync, set enabled: false and call the public syncNow() method on BackupSyncService yourself (exposed as the ${Entity}BackupSyncService provider) from wherever makes sense (an external cron, an endpoint, etc.).

  5. BaseOrmOptions is intentionally left open ({ [key: string]: any }) so the library doesn't depend on any particular project's option shape. Define and use your own typed interface if you want strict typing.

  6. CRUD surface: findOne/find, create/insertMany, updateOne/updateMany, deleteOne/deleteMany cover the most common operations. If you need more (count, exists, aggregate, pagination, etc.), add them to IBaseRepositoryService/ BaseRepositoryService following the same cache/backup propagation pattern, or implement them in a customService.

Known limitation

updateMany re-queries main with the original filter to know which documents to propagate to cache/backups. If update changes a field that's part of filter (e.g. updateMany({ status: 'pending' }, { status: 'done' })), those documents will no longer match and won't be propagated correctly. If this affects you, the workaround is to capture the affected _ids before updating — you can do this by overriding updateMany in a customService.

License

MIT