{"_id":"@deorta-dev/nestjs-repository-core","_rev":"2-e4f44466e016d2bd7ec220e1256cd2fe","name":"@deorta-dev/nestjs-repository-core","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@deorta-dev/nestjs-repository-core","version":"0.1.0","keywords":["nestjs","mongoose","repository","orm","cache","backup","ttl"],"license":"MIT","_id":"@deorta-dev/nestjs-repository-core@0.1.0","maintainers":[{"name":"manueldeortac","email":"manueldeortac@gmail.com"}],"dist":{"shasum":"9623297af80a951dd94a1806fc612f4e9e804bd9","tarball":"https://registry.npmjs.org/@deorta-dev/nestjs-repository-core/-/nestjs-repository-core-0.1.0.tgz","fileCount":64,"integrity":"sha512-7NwiAyYqmY1n9KzcGCvLXsCK93N4KHHYaVe7eF0FV3n0Lmv4xi2y2YCiEPfvo5KQXMUbXQ40sOTL9eYyRkGQig==","signatures":[{"sig":"MEYCIQCnwAylvoaMUwpFy0vF9bmfsXwpYrTaTINwEEPpHS2HnQIhAO3NmInYeIwpQ+0Sf5kLamVmX1D4u2yDfkb4RREpyOcO","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":127365},"main":"dist/index.js","types":"dist/index.d.ts","gitHead":"a0da7850dd3c800f10f2073d307112130e8f53ac","scripts":{"build":"tsc -p tsconfig.build.json","prepublishOnly":"npm run build"},"_npmUser":{"name":"manueldeortac","email":"manueldeortac@gmail.com"},"_npmVersion":"10.8.2","description":"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.","directories":{},"_nodeVersion":"20.20.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"rxjs":"^7.8.1","mongoose":"^8.9.2","typescript":"^5.7.2","@types/node":"^20.17.9","@nestjs/common":"^10.4.15","@nestjs/mongoose":"^10.1.0","reflect-metadata":"^0.1.14","class-transformer":"^0.5.1"},"peerDependencies":{"rxjs":"^7.0.0","mongoose":"^7.0.0 || ^8.0.0","@nestjs/common":"^9.0.0 || ^10.0.0 || ^11.0.0","@nestjs/mongoose":"^9.0.0 || ^10.0.0 || ^11.0.0","reflect-metadata":"^0.1.13 || ^0.2.0","class-transformer":"^0.5.0"},"_npmOperationalInternal":{"tmp":"tmp/nestjs-repository-core_0.1.0_1782716286044_0.4615539576188412","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@deorta-dev/nestjs-repository-core","version":"0.1.1","description":"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.","main":"dist/index.js","types":"dist/index.d.ts","publishConfig":{"access":"public"},"scripts":{"build":"tsc -p tsconfig.build.json","prepublishOnly":"npm run build"},"keywords":["nestjs","mongoose","repository","orm","cache","backup","ttl"],"license":"MIT","peerDependencies":{"@nestjs/common":"^9.0.0 || ^10.0.0 || ^11.0.0","@nestjs/mongoose":"^9.0.0 || ^10.0.0 || ^11.0.0","mongoose":"^7.0.0 || ^8.0.0","class-transformer":"^0.5.0","reflect-metadata":"^0.1.13 || ^0.2.0","rxjs":"^7.0.0"},"devDependencies":{"@nestjs/common":"^10.4.15","@nestjs/mongoose":"^10.1.0","@types/node":"^20.17.9","class-transformer":"^0.5.1","mongoose":"^8.9.2","reflect-metadata":"^0.1.14","rxjs":"^7.8.1","typescript":"^5.7.2"},"_id":"@deorta-dev/nestjs-repository-core@0.1.1","gitHead":"5673829654b2eefb13261ce654059db398971aaf","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-0okMTh7sEjwouGY/ujt+REKDCoOA/CxteefKu4t3nFNqFTm6Apj2Nyu9M+wHFsXh6nL3sTbsuc06mGHNCcqw+Q==","shasum":"0bdf4974b3db166ecccc7dc20825a2762185b3d4","tarball":"https://registry.npmjs.org/@deorta-dev/nestjs-repository-core/-/nestjs-repository-core-0.1.1.tgz","fileCount":64,"unpackedSize":127989,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIGMZ+9pTQC2HOa6IWodADmb96JbmbeWMnaJR8+MDYlQ5AiBK2m5FMCf4jaQKTMmBvaumSa4ua99yeoGapgkIX5FbIw=="}]},"_npmUser":{"name":"manueldeortac","email":"manueldeortac@gmail.com"},"directories":{},"maintainers":[{"name":"manueldeortac","email":"manueldeortac@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/nestjs-repository-core_0.1.1_1782782242627_0.5222443880287757"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-29T06:58:05.900Z","modified":"2026-06-30T01:17:22.894Z","0.1.0":"2026-06-29T06:58:06.173Z","0.1.1":"2026-06-30T01:17:22.769Z"},"license":"MIT","keywords":["nestjs","mongoose","repository","orm","cache","backup","ttl"],"description":"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.","maintainers":[{"name":"manueldeortac","email":"manueldeortac@gmail.com"}],"readme":"**Language:** English · [Español](./README.es.md)\r\n\r\n# @deorta-dev/nestjs-repository-core\r\n\r\nA NestJS + Mongoose library that generates a generic repository service\r\n(`BaseRepositoryService<T>`) for any entity — with read-through caching and\r\nwrite-only backup replicas — **without having to write an `XxxOrmService` /\r\n`XxxOrmModule` class per entity**.\r\n\r\nReplaces the per-entity `PositionOrmService` + `PositionOrmModule` pattern\r\nwith a single `RepositoryOrmModule.register(...)` call.\r\n\r\nBuilt and verified with `tsc --strict` against `@nestjs/common`,\r\n`@nestjs/mongoose`, `mongoose` and `class-transformer`.\r\n\r\n## Installation\r\n\r\n```bash\r\nnpm install @deorta-dev/nestjs-repository-core\r\n```\r\n\r\nYou also need these installed (they're `peerDependencies`, not installed\r\nautomatically):\r\n\r\n```bash\r\nnpm install @nestjs/common @nestjs/mongoose mongoose class-transformer reflect-metadata rxjs\r\n```\r\n\r\n## Basic usage\r\n\r\n```ts\r\nimport { RepositoryOrmModule, RepositoryInject, IBaseRepositoryService } from '@deorta-dev/nestjs-repository-core';\r\n\r\n// position-repository.module.ts\r\nexport const PositionRepositoryModule = RepositoryOrmModule.register({\r\n  entity: Position,\r\n  schema: positionSchema,\r\n  connectionName: ConnectionNames.OPERATION_MDB,\r\n});\r\n\r\n// in any @Module:\r\n@Module({ imports: [PositionRepositoryModule] })\r\nexport class SomeModule {}\r\n\r\n// in any service: inject against the INTERFACE, not the concrete class\r\n// (so swapping in a customService later doesn't require touching this).\r\nconstructor(\r\n  @RepositoryInject(PositionRepositoryModule)\r\n  private readonly positionRepository: IBaseRepositoryService<Position>,\r\n) {}\r\n```\r\n\r\nThe injection token is always `${Entity.name}RepositoryService` (e.g.\r\n`PositionRepositoryService`), so it doesn't matter how many times you call\r\n`.register()` for the same entity — the token is consistent, and\r\n`@RepositoryInject(whateverRegisterReturned)` always resolves to the same\r\nprovider.\r\n\r\nSee `src/examples/position-repository.example.ts` for a complete migration\r\nexample using `Position`.\r\n\r\n## `BaseRepositoryService<T>` API (the `IBaseRepositoryService<T>` contract)\r\n\r\n| Method | What it does |\r\n|---|---|\r\n| `findOne(filter, opts?)` | Cache-first by default; falls back to `main` on a miss. `opts.target = 'main' \\| 'cache'` forces a specific connection (no fallback). |\r\n| `find(filter, opts?)` | Same as `findOne` but returns a list. Supports `opts.sort/limit/skip/projection`. |\r\n| `create(dto)` | Inserts into `main`; the resulting document (with its `_id`) is replicated to cache and all backups. |\r\n| `insertMany(dtos[])` | Same as `create`, in bulk (`insertMany` + `bulkWrite` against cache/backups). |\r\n| `updateOne(filter, update)` | Updates `main` first, then replicates the resulting document to cache and backups. |\r\n| `updateMany(filter, update)` | Same, in bulk. |\r\n| `deleteOne(filter)` / `deleteMany(filter)` | Deletes from `main` first, then from cache and backups, and records a \"tombstone\" so periodic sync knows about it too. |\r\n\r\n## Resilience: what happens if cache or backups are down?\r\n\r\n**As long as `main` is up, the service works** — regardless of the state of\r\n`cache` or any `backup` connection.\r\n\r\n- **Reads (`findOne`/`find`)**: if the cache connection isn't ready or the\r\n  query fails, it's treated as a cache miss and `main` is queried directly —\r\n  the error never propagates.\r\n- **Writes (`create`/`insertMany`/`updateOne`/`updateMany`/`deleteOne`/`deleteMany`)**:\r\n  always run against `main` first. Propagation to `cache` and each `backup`\r\n  is attempted immediately; if a connection isn't ready (`readyState !== 1`)\r\n  or the operation fails, **that write is queued in memory** (one queue per\r\n  secondary connection) instead of failing the whole operation.\r\n- The pending-ops queue retries itself:\r\n    - Every `pendingOps.retryIntervalMs` (default 5000 ms).\r\n    - As soon as the connection emits mongoose's `connected` event (immediate\r\n      reaction, no need to wait for the next tick).\r\n    - If more than `pendingOps.maxQueueSize` operations pile up (default\r\n        1000) because a connection has been down for a while, the oldest ones\r\n              are dropped to avoid unbounded memory growth — that's fine, because\r\n              `BackupSyncService` catches backups up against `main` anyway, and for\r\n              cache, the next `find`/`findOne` simply repopulates it.\r\n- Queued operations are always `_id`-based upserts/deletes (idempotent), so\r\n  retrying them in order, even multiple times, is safe.\r\n- **Each backup is independent**: if you have two backups and one is down,\r\n  the other keeps advancing with its own checkpoint; the one that was down\r\n  catches up on its own once it's back (there's no shared checkpoint that\r\n  one problematic connection can block).\r\n- Tombstones (used to propagate deletes to backups) are only purged from\r\n  the collection once **every** configured backup has already applied them\r\n  — so a backup that was down doesn't lose the information it needs to\r\n  catch up.\r\n\r\n```ts\r\nRepositoryOrmModule.register({\r\n  // ...\r\n  pendingOps: {\r\n    retryIntervalMs: 5000, // how often pending writes are retried\r\n    maxQueueSize: 1000,    // in-memory cap per secondary connection\r\n  },\r\n});\r\n```\r\n\r\n## Custom service (`customService`)\r\n\r\nBy default, `register(...)` uses `BaseRepositoryService`. If you need\r\ndifferent behavior for a particular entity, you can pass your own class via\r\n`customService`:\r\n\r\n```ts\r\nRepositoryOrmModule.register({\r\n  entity: Position,\r\n  schema: positionSchema,\r\n  connectionName: ConnectionNames.OPERATION_MDB,\r\n  customService: PositionRepositoryService, // your class\r\n});\r\n```\r\n\r\n`customService` is typed as `Type<IBaseRepositoryService<T>>`, so\r\n**TypeScript won't let you assign a class that doesn't satisfy the\r\ninterface** (`findOne`, `find`, `create`, `insertMany`, `updateOne`,\r\n`updateMany`, `deleteOne`, `deleteMany`, matching the exact\r\n`IBaseRepositoryService<T>` signatures).\r\n\r\nTwo ways to write one (both shown in\r\n`src/examples/custom-repository-service.example.ts`):\r\n\r\n1. **Extend `BaseRepositoryService<T>`** (recommended): you inherit all the\r\n   cache/backup resilience and only override the method(s) you care about,\r\n   calling `super.method(...)` if you want to keep the original behavior.\r\n\r\n   ```ts\r\n   class PositionRepositoryService extends BaseRepositoryService<Position> {\r\n     async create(dto: Partial<Position>) {\r\n       const created = await super.create(dto);\r\n       console.log('Position created:', created);\r\n       return created;\r\n     }\r\n   }\r\n   ```\r\n\r\n2. **Implement `IBaseRepositoryService<T>` from scratch**: useful if you\r\n   want a completely different strategy (e.g. skip cache/backups\r\n   entirely). Its constructor must accept the same 9 parameters that\r\n   `register(...)` already resolves for you: `entity, options, mainModel,\r\n   cacheModel, cacheConfig, backupModels, backupLabels, tombstoneModel,\r\n   pendingOpsConfig` (even if you don't use all of them).\r\n\r\nEither way, you inject your custom service exactly like the default one,\r\nwith `@RepositoryInject(...)` — nothing else in your code changes, because\r\nboth satisfy `IBaseRepositoryService<T>`.\r\n\r\n## `RepositoryOrmModule.register(...)` configuration reference\r\n\r\n```ts\r\n{\r\n  entity: Position,            // entity class\r\n  schema: positionSchema,      // mongoose schema\r\n  connectionName: '...',       // main connection\r\n  options: {},                 // your BaseOrmOptions\r\n\r\n  cache: {                     // OPTIONAL\r\n    connectionName: '...',\r\n    ttlSeconds: 300,           // how long a document lives in the cache connection\r\n  },\r\n\r\n  backups: [                   // OPTIONAL, array of write-only connections\r\n    { connectionName: '...' },\r\n    { connectionName: '...' },\r\n  ],\r\n\r\n  backupSync: {                // OPTIONAL, only applies if `backups` is set\r\n    enabled: true,              // if false, nothing syncs automatically\r\n    intervalMs: 60_000,         // how often main vs. backups is checked\r\n    runOnStart: true,           // run a check as soon as the module starts\r\n    batchSize: 500,             // documents per batch per check\r\n  },\r\n\r\n  pendingOps: {                // OPTIONAL\r\n    retryIntervalMs: 5000,\r\n    maxQueueSize: 1000,\r\n  },\r\n\r\n  customService: PositionRepositoryService, // OPTIONAL, default: BaseRepositoryService\r\n}\r\n```\r\n\r\n## Design notes\r\n\r\nA few implementation choices worth knowing about if you're extending this\r\nlibrary:\r\n\r\n1. **Cache vs. main on reads**: `findOne`/`find` without an explicit\r\n   `target` query cache first and fall back to `main` on a miss (and\r\n   repopulate cache in the background, without blocking the response).\r\n   With `target: 'main'` or `target: 'cache'`, only that connection is\r\n   queried, with no fallback.\r\n\r\n2. **Cache TTL**: uses an \"expire at a specific time\" TTL index\r\n   (`expireAfterSeconds: 0` on a `_cacheExpiresAt` field) instead of\r\n   classic Mongo TTL, so every write can set its own expiration based on\r\n   `cache.ttlSeconds`, independent of when the index was created.\r\n\r\n3. **How backup catch-up is detected**: for inserts/updates, `updatedTime`\r\n   is compared against a per-entity, per-backup checkpoint (this assumes\r\n   your model keeps `updatedTime` current on every write). For deletes,\r\n   instead of diffing the entire `_id` set (expensive at scale),\r\n   `deleteOne`/`deleteMany` record a \"tombstone\" (`_id` + deletion time) on\r\n   the `main` connection, which the sync process consumes and clears.\r\n   > If your \"deletes\" are actually soft-deletes (a `trashed: true` flag),\r\n   > the tombstone mechanism simply goes unused — `updatedTime`-based sync\r\n   > already covers it, since flipping `trashed` also bumps `updatedTime`.\r\n\r\n4. **Triggering backup sync**: by default, if `backupSync.enabled` is\r\n   `true`, the service starts its own internal `setInterval`\r\n   (`onModuleInit`/`onModuleDestroy`). If you'd rather have a lightweight\r\n   external process decide when to sync, set `enabled: false` and call the\r\n   public `syncNow()` method on `BackupSyncService` yourself (exposed as\r\n   the `${Entity}BackupSyncService` provider) from wherever makes sense\r\n   (an external cron, an endpoint, etc.).\r\n\r\n5. **`BaseOrmOptions`** is intentionally left open\r\n   (`{ [key: string]: any }`) so the library doesn't depend on any\r\n   particular project's option shape. Define and use your own typed\r\n   interface if you want strict typing.\r\n\r\n6. **CRUD surface**: `findOne`/`find`, `create`/`insertMany`,\r\n   `updateOne`/`updateMany`, `deleteOne`/`deleteMany` cover the most common\r\n   operations. If you need more (`count`, `exists`, `aggregate`,\r\n   pagination, etc.), add them to `IBaseRepositoryService`/\r\n   `BaseRepositoryService` following the same cache/backup propagation\r\n   pattern, or implement them in a `customService`.\r\n\r\n## Known limitation\r\n\r\n`updateMany` re-queries `main` with the original `filter` to know which\r\ndocuments to propagate to cache/backups. If `update` changes a field that's\r\npart of `filter` (e.g. `updateMany({ status: 'pending' }, { status: 'done'\r\n})`), those documents will no longer match and won't be propagated\r\ncorrectly. If this affects you, the workaround is to capture the affected\r\n`_id`s *before* updating — you can do this by overriding `updateMany` in a\r\n`customService`.\r\n\r\n## License\r\n\r\nMIT","readmeFilename":"README.md"}