@deorta-dev/nestjs-repository-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.es.md +260 -0
- package/README.md +259 -0
- package/dist/base-repository.service.d.ts +73 -0
- package/dist/base-repository.service.d.ts.map +1 -0
- package/dist/base-repository.service.js +309 -0
- package/dist/base-repository.service.js.map +1 -0
- package/dist/decorators/index.d.ts +2 -0
- package/dist/decorators/index.d.ts.map +1 -0
- package/dist/decorators/index.js +18 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/decorators/repository-inject.decorator.d.ts +20 -0
- package/dist/decorators/repository-inject.decorator.d.ts.map +1 -0
- package/dist/decorators/repository-inject.decorator.js +29 -0
- package/dist/decorators/repository-inject.decorator.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/repository.module.d.ts +13 -0
- package/dist/repository.module.d.ts.map +1 -0
- package/dist/repository.module.js +92 -0
- package/dist/repository.module.js.map +1 -0
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +20 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/sync-checkpoint.schema.d.ts +22 -0
- package/dist/schema/sync-checkpoint.schema.d.ts.map +1 -0
- package/dist/schema/sync-checkpoint.schema.js +13 -0
- package/dist/schema/sync-checkpoint.schema.js.map +1 -0
- package/dist/schema/tombstone.schema.d.ts +24 -0
- package/dist/schema/tombstone.schema.d.ts.map +1 -0
- package/dist/schema/tombstone.schema.js +15 -0
- package/dist/schema/tombstone.schema.js.map +1 -0
- package/dist/schema/with-cache-ttl.d.ts +14 -0
- package/dist/schema/with-cache-ttl.d.ts.map +1 -0
- package/dist/schema/with-cache-ttl.js +24 -0
- package/dist/schema/with-cache-ttl.js.map +1 -0
- package/dist/sync/backup-sync.service.d.ts +61 -0
- package/dist/sync/backup-sync.service.d.ts.map +1 -0
- package/dist/sync/backup-sync.service.js +156 -0
- package/dist/sync/backup-sync.service.js.map +1 -0
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +18 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +19 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/pending-ops-queue.d.ts +44 -0
- package/dist/utils/pending-ops-queue.d.ts.map +1 -0
- package/dist/utils/pending-ops-queue.js +82 -0
- package/dist/utils/pending-ops-queue.js.map +1 -0
- package/dist/utils/repository-token.util.d.ts +7 -0
- package/dist/utils/repository-token.util.d.ts.map +1 -0
- package/dist/utils/repository-token.util.js +21 -0
- package/dist/utils/repository-token.util.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.es.md
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
**Idioma:** Español · [English](./README.md)
|
|
2
|
+
|
|
3
|
+
# @deorta-dev/nestjs-repository-core
|
|
4
|
+
|
|
5
|
+
Librería para NestJS + Mongoose que genera, para cualquier entidad, un
|
|
6
|
+
servicio de repositorio genérico (`BaseRepositoryService<T>`) con caché
|
|
7
|
+
read-through y réplicas de respaldo (backups), **sin tener que crear una
|
|
8
|
+
clase `XxxOrmService` / `XxxOrmModule` por cada entidad**.
|
|
9
|
+
|
|
10
|
+
Reemplaza el patrón de `PositionOrmService` + `PositionOrmModule` por entidad
|
|
11
|
+
con una sola llamada a `RepositoryOrmModule.register(...)`.
|
|
12
|
+
|
|
13
|
+
Compilado y verificado con `tsc --strict` contra `@nestjs/common`,
|
|
14
|
+
`@nestjs/mongoose`, `mongoose` y `class-transformer`.
|
|
15
|
+
|
|
16
|
+
## Instalación
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @deorta-dev/nestjs-repository-core
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Necesitas tener instalados (son `peerDependencies`, no se instalan solos):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @nestjs/common @nestjs/mongoose mongoose class-transformer reflect-metadata rxjs
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Uso básico
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { RepositoryOrmModule, RepositoryInject, IBaseRepositoryService } from '@deorta-dev/nestjs-repository-core';
|
|
32
|
+
|
|
33
|
+
// position-repository.module.ts
|
|
34
|
+
export const PositionRepositoryModule = RepositoryOrmModule.register({
|
|
35
|
+
entity: Position,
|
|
36
|
+
schema: positionSchema,
|
|
37
|
+
connectionName: ConnectionNames.OPERATION_MDB,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// en cualquier @Module:
|
|
41
|
+
@Module({ imports: [PositionRepositoryModule] })
|
|
42
|
+
export class SomeModule {}
|
|
43
|
+
|
|
44
|
+
// en cualquier servicio: inyecta contra la INTERFAZ, no contra la clase concreta
|
|
45
|
+
// (así, si luego cambias a un customService, no tienes que tocar nada aquí).
|
|
46
|
+
constructor(
|
|
47
|
+
@RepositoryInject(PositionRepositoryModule)
|
|
48
|
+
private readonly positionRepository: IBaseRepositoryService<Position>,
|
|
49
|
+
) {}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
El token de inyección siempre es `${Entidad.name}RepositoryService` (ej.
|
|
53
|
+
`PositionRepositoryService`), así que no importa cuántas veces llames
|
|
54
|
+
`.register()` para la misma entidad: el token es consistente y
|
|
55
|
+
`@RepositoryInject(loQueSeaQueDevolvióRegister)` siempre apunta al mismo
|
|
56
|
+
provider.
|
|
57
|
+
|
|
58
|
+
Ver `src/examples/position-repository.example.ts` para el ejemplo completo
|
|
59
|
+
migrando `Position`.
|
|
60
|
+
|
|
61
|
+
## API de `BaseRepositoryService<T>` (contrato `IBaseRepositoryService<T>`)
|
|
62
|
+
|
|
63
|
+
| Método | Qué hace |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `findOne(filter, opts?)` | Por defecto: caché primero, si no encuentra cae a `main` y repuebla caché. `opts.target = 'main' \| 'cache'` para forzar una conexión específica (sin fallback). |
|
|
66
|
+
| `find(filter, opts?)` | Igual que `findOne` pero lista. Soporta `opts.sort/limit/skip/projection`. |
|
|
67
|
+
| `create(dto)` | Inserta en `main`; el documento resultante (con su `_id`) se replica en caché y en todos los backups. |
|
|
68
|
+
| `insertMany(dtos[])` | Igual que `create` pero en bulk (`insertMany` + `bulkWrite` hacia caché/backups). |
|
|
69
|
+
| `updateOne(filter, update)` | Actualiza `main` primero, luego replica el documento resultante en caché y backups. |
|
|
70
|
+
| `updateMany(filter, update)` | Igual en bulk. |
|
|
71
|
+
| `deleteOne(filter)` / `deleteMany(filter)` | Borra en `main` primero, luego en caché y backups, y registra un "tombstone" para que la sincronización periódica también lo sepa. |
|
|
72
|
+
|
|
73
|
+
## Resiliencia: ¿qué pasa si caché o backups están caídos?
|
|
74
|
+
|
|
75
|
+
**Si `main` funciona, el servicio funciona**, sin importar el estado de
|
|
76
|
+
`cache` o de los `backups`.
|
|
77
|
+
|
|
78
|
+
- **Lecturas (`findOne`/`find`)**: si la conexión de caché no está lista o la
|
|
79
|
+
consulta falla, se trata como "cache miss" y se consulta `main`
|
|
80
|
+
directamente — nunca se propaga el error.
|
|
81
|
+
- **Escrituras (`create`/`insertMany`/`updateOne`/`updateMany`/`deleteOne`/`deleteMany`)**:
|
|
82
|
+
siempre se ejecutan en `main` primero. La propagación hacia `cache` y cada
|
|
83
|
+
`backup` se intenta de inmediato; si una conexión no está lista
|
|
84
|
+
(`readyState !== 1`) o la operación falla, **esa operación queda pendiente
|
|
85
|
+
en una cola en memoria** (una por conexión secundaria) en vez de hacer
|
|
86
|
+
fallar la operación completa.
|
|
87
|
+
- La cola de pendientes se reintenta sola:
|
|
88
|
+
- Cada `pendingOps.retryIntervalMs` (default 5000 ms).
|
|
89
|
+
- Apenas la conexión emite el evento `connected` de mongoose (reacción
|
|
90
|
+
inmediata, no espera al próximo tick).
|
|
91
|
+
- Si se acumulan más de `pendingOps.maxQueueSize` operaciones (default
|
|
92
|
+
1000) porque una conexión estuvo caída mucho tiempo, se descartan las
|
|
93
|
+
más antiguas para no consumir memoria indefinidamente — eso está bien
|
|
94
|
+
porque, para backups, `BackupSyncService` los pone al día de todos
|
|
95
|
+
modos comparando contra `main`; y para caché, el próximo `find`/`findOne`
|
|
96
|
+
simplemente la repuebla.
|
|
97
|
+
- Las operaciones encoladas son siempre upserts/deletes por `_id`
|
|
98
|
+
(idempotentes), así que reintentarlas en orden, incluso varias veces, es
|
|
99
|
+
seguro.
|
|
100
|
+
- **Cada backup es independiente**: si tienes dos backups y uno está caído,
|
|
101
|
+
el otro sigue avanzando con su propio checkpoint; el caído se pondrá al
|
|
102
|
+
día solo cuando vuelva (no hay un checkpoint compartido que se bloquee por
|
|
103
|
+
una sola conexión problemática).
|
|
104
|
+
- Los tombstones (usados para propagar deletes a los backups) solo se borran
|
|
105
|
+
de la colección una vez que **todos** los backups configurados ya los
|
|
106
|
+
aplicaron — así uno que estuvo caído no se queda sin la información que
|
|
107
|
+
necesita para ponerse al día.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
RepositoryOrmModule.register({
|
|
111
|
+
// ...
|
|
112
|
+
pendingOps: {
|
|
113
|
+
retryIntervalMs: 5000, // cada cuánto se reintentan las operaciones pendientes
|
|
114
|
+
maxQueueSize: 1000, // tope en memoria por conexión secundaria
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Servicio personalizado (`customService`)
|
|
120
|
+
|
|
121
|
+
Por defecto, `register(...)` usa `BaseRepositoryService`. Si necesitas una
|
|
122
|
+
lógica distinta para una entidad en particular, puedes pasar tu propia clase
|
|
123
|
+
en `customService`:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
RepositoryOrmModule.register({
|
|
127
|
+
entity: Position,
|
|
128
|
+
schema: positionSchema,
|
|
129
|
+
connectionName: ConnectionNames.OPERATION_MDB,
|
|
130
|
+
customService: PositionRepositoryService, // tu clase
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`customService` está tipado como `Type<IBaseRepositoryService<T>>`, así que
|
|
135
|
+
**TypeScript no te deja asignar ahí una clase que no cumpla la interfaz**
|
|
136
|
+
(`findOne`, `find`, `create`, `insertMany`, `updateOne`, `updateMany`,
|
|
137
|
+
`deleteOne`, `deleteMany`, con las firmas exactas de `IBaseRepositoryService<T>`).
|
|
138
|
+
|
|
139
|
+
Dos formas de escribirla (ambas en `src/examples/custom-repository-service.example.ts`):
|
|
140
|
+
|
|
141
|
+
1. **Extender `BaseRepositoryService<T>`** (recomendado): heredas toda la
|
|
142
|
+
resiliencia de caché/backups y solo sobreescribes el método que te
|
|
143
|
+
interese, llamando `super.metodo(...)` si quieres conservar el
|
|
144
|
+
comportamiento original.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
class PositionRepositoryService extends BaseRepositoryService<Position> {
|
|
148
|
+
async create(dto: Partial<Position>) {
|
|
149
|
+
const created = await super.create(dto);
|
|
150
|
+
console.log('Position creada:', created);
|
|
151
|
+
return created;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
2. **Implementar `IBaseRepositoryService<T>` desde cero**: útil si quieres
|
|
157
|
+
una estrategia totalmente distinta (ej. ignorar caché/backups). El
|
|
158
|
+
constructor que recibe debe aceptar los mismos 9 parámetros que
|
|
159
|
+
`register(...)` ya resuelve por ti: `entity, options, mainModel,
|
|
160
|
+
cacheModel, cacheConfig, backupModels, backupLabels, tombstoneModel,
|
|
161
|
+
pendingOpsConfig` (aunque no los uses todos).
|
|
162
|
+
|
|
163
|
+
Sea cualquiera de las dos, inyectas tu servicio personalizado exactamente
|
|
164
|
+
igual que el default, con `@RepositoryInject(...)` — no cambia nada en el
|
|
165
|
+
resto de tu código, porque ambos cumplen `IBaseRepositoryService<T>`.
|
|
166
|
+
|
|
167
|
+
## Configuración de `RepositoryOrmModule.register(...)`
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
{
|
|
171
|
+
entity: Position, // clase de la entidad
|
|
172
|
+
schema: positionSchema, // schema de mongoose
|
|
173
|
+
connectionName: '...', // conexión principal
|
|
174
|
+
options: {}, // tus BaseOrmOptions actuales
|
|
175
|
+
|
|
176
|
+
cache: { // OPCIONAL
|
|
177
|
+
connectionName: '...',
|
|
178
|
+
ttlSeconds: 300, // TTL del documento en la conexión de caché
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
backups: [ // OPCIONAL, array de conexiones de solo-escritura
|
|
182
|
+
{ connectionName: '...' },
|
|
183
|
+
{ connectionName: '...' },
|
|
184
|
+
],
|
|
185
|
+
|
|
186
|
+
backupSync: { // OPCIONAL, solo aplica si hay `backups`
|
|
187
|
+
enabled: true, // si es false, nadie sincroniza automáticamente
|
|
188
|
+
intervalMs: 60_000, // cada cuánto se revisa main vs backups
|
|
189
|
+
runOnStart: true, // corre una verificación apenas arranca el módulo
|
|
190
|
+
batchSize: 500, // documentos por lote en cada verificación
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
pendingOps: { // OPCIONAL
|
|
194
|
+
retryIntervalMs: 5000,
|
|
195
|
+
maxQueueSize: 1000,
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
customService: PositionRepositoryService, // OPCIONAL, default: BaseRepositoryService
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Notas de diseño
|
|
203
|
+
|
|
204
|
+
Algunas decisiones de implementación que vale la pena conocer si vas a
|
|
205
|
+
extender la librería:
|
|
206
|
+
|
|
207
|
+
1. **Caché vs. main en lecturas**: `findOne`/`find` sin `target` explícito
|
|
208
|
+
consultan primero caché y si no hay nada caen a `main` (y repueblan la
|
|
209
|
+
caché en segundo plano, sin bloquear la respuesta). Con `target: 'main'`
|
|
210
|
+
o `target: 'cache'` consultan *solo* esa conexión, sin fallback.
|
|
211
|
+
|
|
212
|
+
2. **TTL de caché**: usa un índice TTL "a fecha exacta"
|
|
213
|
+
(`expireAfterSeconds: 0` sobre un campo `_cacheExpiresAt`) en vez del TTL
|
|
214
|
+
clásico de Mongo, porque así cada escritura define su propio vencimiento
|
|
215
|
+
según `cache.ttlSeconds`, sin depender de cuándo se creó el índice.
|
|
216
|
+
|
|
217
|
+
3. **Cómo se detecta qué le falta a un backup**: para inserts/updates se
|
|
218
|
+
compara `updatedTime` contra un checkpoint guardado por entidad (asume
|
|
219
|
+
que tu modelo mantiene `updatedTime` actualizado en cada escritura). Para
|
|
220
|
+
deletes, en vez de comparar todo el set de `_id` (caro a escala),
|
|
221
|
+
`deleteOne`/`deleteMany` registran un "tombstone" (`_id` + fecha de
|
|
222
|
+
borrado) en la conexión `main`, y el sync lo consume y lo borra.
|
|
223
|
+
> Si tus "deletes" en realidad son soft-deletes (un flag como
|
|
224
|
+
> `trashed: true`), el mecanismo de tombstones simplemente no se usa —
|
|
225
|
+
> la sincronización por `updatedTime` ya es suficiente, porque marcar
|
|
226
|
+
> `trashed: true` también actualiza `updatedTime`.
|
|
227
|
+
|
|
228
|
+
4. **Disparo de la sincronización de backups**: por defecto, si
|
|
229
|
+
`backupSync.enabled` es `true`, el propio servicio arranca un
|
|
230
|
+
`setInterval` interno (`onModuleInit`/`onModuleDestroy`). Si prefieres que
|
|
231
|
+
un proceso externo de bajo esfuerzo decida cuándo sincronizar, deja
|
|
232
|
+
`enabled: false` y llama tú mismo al método público `syncNow()` del
|
|
233
|
+
`BackupSyncService` (expuesto como provider `${Entidad}BackupSyncService`)
|
|
234
|
+
desde donde quieras (cron externo, endpoint, etc.).
|
|
235
|
+
|
|
236
|
+
5. **`BaseOrmOptions`** se deja intencionalmente abierta
|
|
237
|
+
(`{ [key: string]: any }`) para que la librería no dependa de la forma
|
|
238
|
+
exacta de opciones de ningún proyecto en particular. Si quieres tipado
|
|
239
|
+
estricto, define tu propia interfaz y úsala en su lugar.
|
|
240
|
+
|
|
241
|
+
6. **Superficie de CRUD**: `findOne`/`find`, `create`/`insertMany`,
|
|
242
|
+
`updateOne`/`updateMany`, `deleteOne`/`deleteMany` cubren las operaciones
|
|
243
|
+
más comunes. Si necesitas más (`count`, `exists`, `aggregate`,
|
|
244
|
+
paginación, etc.), agrégalas a `IBaseRepositoryService`/
|
|
245
|
+
`BaseRepositoryService` siguiendo el mismo patrón de propagación a
|
|
246
|
+
caché/backups, o impleméntalas en un `customService`.
|
|
247
|
+
|
|
248
|
+
## Limitación conocida
|
|
249
|
+
|
|
250
|
+
En `updateMany`, para propagar a caché/backups se vuelve a consultar `main`
|
|
251
|
+
con el mismo `filter` original. Si el `update` cambia campos que forman
|
|
252
|
+
parte de ese `filter` (ej. `updateMany({ status: 'pending' }, { status:
|
|
253
|
+
'done' })`), esos documentos ya no harán match y no se propagarán
|
|
254
|
+
correctamente. Si esto te afecta, la alternativa es capturar los `_id`
|
|
255
|
+
afectados *antes* de actualizar — puedes hacerlo sobreescribiendo
|
|
256
|
+
`updateMany` en un `customService`.
|
|
257
|
+
|
|
258
|
+
## Licencia
|
|
259
|
+
|
|
260
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
**Language:** English · [Español](./README.es.md)
|
|
2
|
+
|
|
3
|
+
# @deorta-dev/nestjs-repository-core
|
|
4
|
+
|
|
5
|
+
A NestJS + Mongoose library that generates a generic repository service
|
|
6
|
+
(`BaseRepositoryService<T>`) for any entity — with read-through caching and
|
|
7
|
+
write-only backup replicas — **without having to write an `XxxOrmService` /
|
|
8
|
+
`XxxOrmModule` class per entity**.
|
|
9
|
+
|
|
10
|
+
Replaces the per-entity `PositionOrmService` + `PositionOrmModule` pattern
|
|
11
|
+
with a single `RepositoryOrmModule.register(...)` call.
|
|
12
|
+
|
|
13
|
+
Built and verified with `tsc --strict` against `@nestjs/common`,
|
|
14
|
+
`@nestjs/mongoose`, `mongoose` and `class-transformer`.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @deorta-dev/nestjs-repository-core
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
You also need these installed (they're `peerDependencies`, not installed
|
|
23
|
+
automatically):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @nestjs/common @nestjs/mongoose mongoose class-transformer reflect-metadata rxjs
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Basic usage
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { RepositoryOrmModule, RepositoryInject, IBaseRepositoryService } from '@deorta-dev/nestjs-repository-core';
|
|
33
|
+
|
|
34
|
+
// position-repository.module.ts
|
|
35
|
+
export const PositionRepositoryModule = RepositoryOrmModule.register({
|
|
36
|
+
entity: Position,
|
|
37
|
+
schema: positionSchema,
|
|
38
|
+
connectionName: ConnectionNames.OPERATION_MDB,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// in any @Module:
|
|
42
|
+
@Module({ imports: [PositionRepositoryModule] })
|
|
43
|
+
export class SomeModule {}
|
|
44
|
+
|
|
45
|
+
// in any service: inject against the INTERFACE, not the concrete class
|
|
46
|
+
// (so swapping in a customService later doesn't require touching this).
|
|
47
|
+
constructor(
|
|
48
|
+
@RepositoryInject(PositionRepositoryModule)
|
|
49
|
+
private readonly positionRepository: IBaseRepositoryService<Position>,
|
|
50
|
+
) {}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The injection token is always `${Entity.name}RepositoryService` (e.g.
|
|
54
|
+
`PositionRepositoryService`), so it doesn't matter how many times you call
|
|
55
|
+
`.register()` for the same entity — the token is consistent, and
|
|
56
|
+
`@RepositoryInject(whateverRegisterReturned)` always resolves to the same
|
|
57
|
+
provider.
|
|
58
|
+
|
|
59
|
+
See `src/examples/position-repository.example.ts` for a complete migration
|
|
60
|
+
example using `Position`.
|
|
61
|
+
|
|
62
|
+
## `BaseRepositoryService<T>` API (the `IBaseRepositoryService<T>` contract)
|
|
63
|
+
|
|
64
|
+
| Method | What it does |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `findOne(filter, opts?)` | Cache-first by default; falls back to `main` on a miss. `opts.target = 'main' \| 'cache'` forces a specific connection (no fallback). |
|
|
67
|
+
| `find(filter, opts?)` | Same as `findOne` but returns a list. Supports `opts.sort/limit/skip/projection`. |
|
|
68
|
+
| `create(dto)` | Inserts into `main`; the resulting document (with its `_id`) is replicated to cache and all backups. |
|
|
69
|
+
| `insertMany(dtos[])` | Same as `create`, in bulk (`insertMany` + `bulkWrite` against cache/backups). |
|
|
70
|
+
| `updateOne(filter, update)` | Updates `main` first, then replicates the resulting document to cache and backups. |
|
|
71
|
+
| `updateMany(filter, update)` | Same, in bulk. |
|
|
72
|
+
| `deleteOne(filter)` / `deleteMany(filter)` | Deletes from `main` first, then from cache and backups, and records a "tombstone" so periodic sync knows about it too. |
|
|
73
|
+
|
|
74
|
+
## Resilience: what happens if cache or backups are down?
|
|
75
|
+
|
|
76
|
+
**As long as `main` is up, the service works** — regardless of the state of
|
|
77
|
+
`cache` or any `backup` connection.
|
|
78
|
+
|
|
79
|
+
- **Reads (`findOne`/`find`)**: if the cache connection isn't ready or the
|
|
80
|
+
query fails, it's treated as a cache miss and `main` is queried directly —
|
|
81
|
+
the error never propagates.
|
|
82
|
+
- **Writes (`create`/`insertMany`/`updateOne`/`updateMany`/`deleteOne`/`deleteMany`)**:
|
|
83
|
+
always run against `main` first. Propagation to `cache` and each `backup`
|
|
84
|
+
is attempted immediately; if a connection isn't ready (`readyState !== 1`)
|
|
85
|
+
or the operation fails, **that write is queued in memory** (one queue per
|
|
86
|
+
secondary connection) instead of failing the whole operation.
|
|
87
|
+
- The pending-ops queue retries itself:
|
|
88
|
+
- Every `pendingOps.retryIntervalMs` (default 5000 ms).
|
|
89
|
+
- As soon as the connection emits mongoose's `connected` event (immediate
|
|
90
|
+
reaction, no need to wait for the next tick).
|
|
91
|
+
- If more than `pendingOps.maxQueueSize` operations pile up (default
|
|
92
|
+
1000) because a connection has been down for a while, the oldest ones
|
|
93
|
+
are dropped to avoid unbounded memory growth — that's fine, because
|
|
94
|
+
`BackupSyncService` catches backups up against `main` anyway, and for
|
|
95
|
+
cache, the next `find`/`findOne` simply repopulates it.
|
|
96
|
+
- Queued operations are always `_id`-based upserts/deletes (idempotent), so
|
|
97
|
+
retrying them in order, even multiple times, is safe.
|
|
98
|
+
- **Each backup is independent**: if you have two backups and one is down,
|
|
99
|
+
the other keeps advancing with its own checkpoint; the one that was down
|
|
100
|
+
catches up on its own once it's back (there's no shared checkpoint that
|
|
101
|
+
one problematic connection can block).
|
|
102
|
+
- Tombstones (used to propagate deletes to backups) are only purged from
|
|
103
|
+
the collection once **every** configured backup has already applied them
|
|
104
|
+
— so a backup that was down doesn't lose the information it needs to
|
|
105
|
+
catch up.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
RepositoryOrmModule.register({
|
|
109
|
+
// ...
|
|
110
|
+
pendingOps: {
|
|
111
|
+
retryIntervalMs: 5000, // how often pending writes are retried
|
|
112
|
+
maxQueueSize: 1000, // in-memory cap per secondary connection
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Custom service (`customService`)
|
|
118
|
+
|
|
119
|
+
By default, `register(...)` uses `BaseRepositoryService`. If you need
|
|
120
|
+
different behavior for a particular entity, you can pass your own class via
|
|
121
|
+
`customService`:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
RepositoryOrmModule.register({
|
|
125
|
+
entity: Position,
|
|
126
|
+
schema: positionSchema,
|
|
127
|
+
connectionName: ConnectionNames.OPERATION_MDB,
|
|
128
|
+
customService: PositionRepositoryService, // your class
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`customService` is typed as `Type<IBaseRepositoryService<T>>`, so
|
|
133
|
+
**TypeScript won't let you assign a class that doesn't satisfy the
|
|
134
|
+
interface** (`findOne`, `find`, `create`, `insertMany`, `updateOne`,
|
|
135
|
+
`updateMany`, `deleteOne`, `deleteMany`, matching the exact
|
|
136
|
+
`IBaseRepositoryService<T>` signatures).
|
|
137
|
+
|
|
138
|
+
Two ways to write one (both shown in
|
|
139
|
+
`src/examples/custom-repository-service.example.ts`):
|
|
140
|
+
|
|
141
|
+
1. **Extend `BaseRepositoryService<T>`** (recommended): you inherit all the
|
|
142
|
+
cache/backup resilience and only override the method(s) you care about,
|
|
143
|
+
calling `super.method(...)` if you want to keep the original behavior.
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
class PositionRepositoryService extends BaseRepositoryService<Position> {
|
|
147
|
+
async create(dto: Partial<Position>) {
|
|
148
|
+
const created = await super.create(dto);
|
|
149
|
+
console.log('Position created:', created);
|
|
150
|
+
return created;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
2. **Implement `IBaseRepositoryService<T>` from scratch**: useful if you
|
|
156
|
+
want a completely different strategy (e.g. skip cache/backups
|
|
157
|
+
entirely). Its constructor must accept the same 9 parameters that
|
|
158
|
+
`register(...)` already resolves for you: `entity, options, mainModel,
|
|
159
|
+
cacheModel, cacheConfig, backupModels, backupLabels, tombstoneModel,
|
|
160
|
+
pendingOpsConfig` (even if you don't use all of them).
|
|
161
|
+
|
|
162
|
+
Either way, you inject your custom service exactly like the default one,
|
|
163
|
+
with `@RepositoryInject(...)` — nothing else in your code changes, because
|
|
164
|
+
both satisfy `IBaseRepositoryService<T>`.
|
|
165
|
+
|
|
166
|
+
## `RepositoryOrmModule.register(...)` configuration reference
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
{
|
|
170
|
+
entity: Position, // entity class
|
|
171
|
+
schema: positionSchema, // mongoose schema
|
|
172
|
+
connectionName: '...', // main connection
|
|
173
|
+
options: {}, // your BaseOrmOptions
|
|
174
|
+
|
|
175
|
+
cache: { // OPTIONAL
|
|
176
|
+
connectionName: '...',
|
|
177
|
+
ttlSeconds: 300, // how long a document lives in the cache connection
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
backups: [ // OPTIONAL, array of write-only connections
|
|
181
|
+
{ connectionName: '...' },
|
|
182
|
+
{ connectionName: '...' },
|
|
183
|
+
],
|
|
184
|
+
|
|
185
|
+
backupSync: { // OPTIONAL, only applies if `backups` is set
|
|
186
|
+
enabled: true, // if false, nothing syncs automatically
|
|
187
|
+
intervalMs: 60_000, // how often main vs. backups is checked
|
|
188
|
+
runOnStart: true, // run a check as soon as the module starts
|
|
189
|
+
batchSize: 500, // documents per batch per check
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
pendingOps: { // OPTIONAL
|
|
193
|
+
retryIntervalMs: 5000,
|
|
194
|
+
maxQueueSize: 1000,
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
customService: PositionRepositoryService, // OPTIONAL, default: BaseRepositoryService
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Design notes
|
|
202
|
+
|
|
203
|
+
A few implementation choices worth knowing about if you're extending this
|
|
204
|
+
library:
|
|
205
|
+
|
|
206
|
+
1. **Cache vs. main on reads**: `findOne`/`find` without an explicit
|
|
207
|
+
`target` query cache first and fall back to `main` on a miss (and
|
|
208
|
+
repopulate cache in the background, without blocking the response).
|
|
209
|
+
With `target: 'main'` or `target: 'cache'`, only that connection is
|
|
210
|
+
queried, with no fallback.
|
|
211
|
+
|
|
212
|
+
2. **Cache TTL**: uses an "expire at a specific time" TTL index
|
|
213
|
+
(`expireAfterSeconds: 0` on a `_cacheExpiresAt` field) instead of
|
|
214
|
+
classic Mongo TTL, so every write can set its own expiration based on
|
|
215
|
+
`cache.ttlSeconds`, independent of when the index was created.
|
|
216
|
+
|
|
217
|
+
3. **How backup catch-up is detected**: for inserts/updates, `updatedTime`
|
|
218
|
+
is compared against a per-entity, per-backup checkpoint (this assumes
|
|
219
|
+
your model keeps `updatedTime` current on every write). For deletes,
|
|
220
|
+
instead of diffing the entire `_id` set (expensive at scale),
|
|
221
|
+
`deleteOne`/`deleteMany` record a "tombstone" (`_id` + deletion time) on
|
|
222
|
+
the `main` connection, which the sync process consumes and clears.
|
|
223
|
+
> If your "deletes" are actually soft-deletes (a `trashed: true` flag),
|
|
224
|
+
> the tombstone mechanism simply goes unused — `updatedTime`-based sync
|
|
225
|
+
> already covers it, since flipping `trashed` also bumps `updatedTime`.
|
|
226
|
+
|
|
227
|
+
4. **Triggering backup sync**: by default, if `backupSync.enabled` is
|
|
228
|
+
`true`, the service starts its own internal `setInterval`
|
|
229
|
+
(`onModuleInit`/`onModuleDestroy`). If you'd rather have a lightweight
|
|
230
|
+
external process decide when to sync, set `enabled: false` and call the
|
|
231
|
+
public `syncNow()` method on `BackupSyncService` yourself (exposed as
|
|
232
|
+
the `${Entity}BackupSyncService` provider) from wherever makes sense
|
|
233
|
+
(an external cron, an endpoint, etc.).
|
|
234
|
+
|
|
235
|
+
5. **`BaseOrmOptions`** is intentionally left open
|
|
236
|
+
(`{ [key: string]: any }`) so the library doesn't depend on any
|
|
237
|
+
particular project's option shape. Define and use your own typed
|
|
238
|
+
interface if you want strict typing.
|
|
239
|
+
|
|
240
|
+
6. **CRUD surface**: `findOne`/`find`, `create`/`insertMany`,
|
|
241
|
+
`updateOne`/`updateMany`, `deleteOne`/`deleteMany` cover the most common
|
|
242
|
+
operations. If you need more (`count`, `exists`, `aggregate`,
|
|
243
|
+
pagination, etc.), add them to `IBaseRepositoryService`/
|
|
244
|
+
`BaseRepositoryService` following the same cache/backup propagation
|
|
245
|
+
pattern, or implement them in a `customService`.
|
|
246
|
+
|
|
247
|
+
## Known limitation
|
|
248
|
+
|
|
249
|
+
`updateMany` re-queries `main` with the original `filter` to know which
|
|
250
|
+
documents to propagate to cache/backups. If `update` changes a field that's
|
|
251
|
+
part of `filter` (e.g. `updateMany({ status: 'pending' }, { status: 'done'
|
|
252
|
+
})`), those documents will no longer match and won't be propagated
|
|
253
|
+
correctly. If this affects you, the workaround is to capture the affected
|
|
254
|
+
`_id`s *before* updating — you can do this by overriding `updateMany` in a
|
|
255
|
+
`customService`.
|
|
256
|
+
|
|
257
|
+
## License
|
|
258
|
+
|
|
259
|
+
MIT
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Logger, OnModuleDestroy, OnModuleInit, Type } from '@nestjs/common';
|
|
2
|
+
import { FilterQuery, Model, UpdateQuery } from 'mongoose';
|
|
3
|
+
import { BaseOrmOptions, CacheConnectionConfig, FindOptions, PendingOpsConfig } from './types';
|
|
4
|
+
import { PendingOpsQueue } from './utils';
|
|
5
|
+
import { Observable } from 'rxjs';
|
|
6
|
+
interface PropagationEntry {
|
|
7
|
+
model: Model<any>;
|
|
8
|
+
queue: PendingOpsQueue;
|
|
9
|
+
label: string;
|
|
10
|
+
op: () => Promise<any>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Reemplazo genérico de los antiguos `XxxOrmService` (ej. PositionOrmService).
|
|
14
|
+
* Una sola clase sirve para cualquier entidad: la instancia la crea
|
|
15
|
+
* `RepositoryOrmModule.register(...)`, tú nunca extiendes esta clase.
|
|
16
|
+
*
|
|
17
|
+
* Resiliencia: si la conexión de caché o alguna de backup no está lista o
|
|
18
|
+
* falla al escribir, la operación sobre `main` se completa igual y la
|
|
19
|
+
* escritura hacia esa conexión secundaria queda pendiente en una cola en
|
|
20
|
+
* memoria que se reintenta sola (ver `utils/pending-ops-queue.ts`).
|
|
21
|
+
*/
|
|
22
|
+
export declare class BaseRepositoryService<T = any> implements OnModuleInit, OnModuleDestroy {
|
|
23
|
+
protected readonly entity: Type<T>;
|
|
24
|
+
protected readonly options: BaseOrmOptions;
|
|
25
|
+
protected readonly mainModel: Model<T>;
|
|
26
|
+
protected readonly cacheModel: Model<T> | undefined;
|
|
27
|
+
protected readonly cacheConfig: CacheConnectionConfig | undefined;
|
|
28
|
+
protected readonly backupModels: Model<T>[];
|
|
29
|
+
protected readonly tombstoneModel: Model<any> | undefined;
|
|
30
|
+
protected readonly logger: Logger;
|
|
31
|
+
protected readonly cacheLabel?: string;
|
|
32
|
+
protected readonly cacheQueue?: PendingOpsQueue;
|
|
33
|
+
protected readonly backupLabels: string[];
|
|
34
|
+
protected readonly backupQueues: PendingOpsQueue[];
|
|
35
|
+
constructor(entity: Type<T>, options: BaseOrmOptions, mainModel: Model<T>, cacheModel: Model<T> | undefined, cacheConfig: CacheConnectionConfig | undefined, backupModels: Model<T>[] | undefined, backupConnectionNames: string[] | undefined, tombstoneModel: Model<any> | undefined, pendingOpsConfig?: PendingOpsConfig);
|
|
36
|
+
onModuleInit(): void;
|
|
37
|
+
onModuleDestroy(): void;
|
|
38
|
+
findOne(filter: FilterQuery<T>, opts?: FindOptions): Observable<T | null>;
|
|
39
|
+
find(filter?: FilterQuery<T>, opts?: FindOptions): Observable<T[]>;
|
|
40
|
+
count(filter?: FilterQuery<T>): Observable<number>;
|
|
41
|
+
aggregate<R = any>(pipeline: any[]): Observable<R[]>;
|
|
42
|
+
create(dto: Partial<T>): Observable<T>;
|
|
43
|
+
insertMany(dtos: Partial<T>[]): Observable<T[]>;
|
|
44
|
+
updateOne(filter: FilterQuery<T>, update: UpdateQuery<T>): Observable<T | null>;
|
|
45
|
+
updateMany(filter: FilterQuery<T>, update: UpdateQuery<T>): Observable<{
|
|
46
|
+
matched: number;
|
|
47
|
+
modified: number;
|
|
48
|
+
}>;
|
|
49
|
+
updateObject(object: any): Observable<T | null>;
|
|
50
|
+
upsertBulk(bulkData: any[]): Observable<any>;
|
|
51
|
+
updateBulk(bulkData: any[]): Observable<any>;
|
|
52
|
+
deleteOne(filter: FilterQuery<T>): Observable<boolean>;
|
|
53
|
+
deleteMany(filter: FilterQuery<T>): Observable<number>;
|
|
54
|
+
protected propagate(entries: PropagationEntry[]): Observable<void>;
|
|
55
|
+
protected guard(model: Model<any>, label: string, op: () => Promise<any>): () => Promise<void>;
|
|
56
|
+
protected buildSingleEntries(doc: any, kind: 'upsert' | 'delete'): PropagationEntry[];
|
|
57
|
+
protected buildBulkEntries(docs: any[], kind: 'upsert' | 'delete'): PropagationEntry[];
|
|
58
|
+
protected tryCacheRead<R>(fn: () => Promise<R>): Observable<R | undefined>;
|
|
59
|
+
protected scheduleWriteToCache(doc: any): void;
|
|
60
|
+
protected withUpdatedTime(update: UpdateQuery<T>): UpdateQuery<T>;
|
|
61
|
+
protected upsertInto(model: Model<T>, doc: any): Promise<void>;
|
|
62
|
+
protected bulkUpsertInto(model: Model<any>, docs: any[], isCache: boolean): Promise<void>;
|
|
63
|
+
protected withCacheExpiry(doc: any): any;
|
|
64
|
+
protected recordTombstone(id: any): Promise<void>;
|
|
65
|
+
protected applyCursorOptions<Q extends {
|
|
66
|
+
sort: Function;
|
|
67
|
+
skip: Function;
|
|
68
|
+
limit: Function;
|
|
69
|
+
}>(query: Q, opts: FindOptions): Q;
|
|
70
|
+
protected toEntity(doc: any): T;
|
|
71
|
+
}
|
|
72
|
+
export {};
|
|
73
|
+
//# sourceMappingURL=base-repository.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-repository.service.d.ts","sourceRoot":"","sources":["../src/base-repository.service.ts"],"names":[],"mappings":"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"}
|