Repositories y acceso a datos con MongoDB | Nicolás Garzón
Texto
Copiar caso de uso
→ repository orientada al dominio
→ driver o Mongoose
→ MongoDBEl objetivo no es ocultar MongoDB, sino impedir que controllers y servicios construyan queries inseguras o inconsistentes.
TypeScript
Copiar repository. find ( filter) ;
repository. update ( id, data) ; TypeScript
Copiar findPendingOrdersForBranch ( {
businessId,
branchId,
cursor,
limit,
session
} ) ; La firma expresa intención, scope y pagination. El caller no controla operadores arbitrarios.
Un repositorio genérico suele reducir todo a:
findById;
findAll;
create;
update;
delete.
MongoDB necesita capacidades específicas:
$inc;
conditional updates;
aggregation;
bulkWrite;
projections;
cursor pagination;
sessions;
read/write concerns;
Change Streams.
Ocultarlas produce abstracciones débiles o escapes constantes.
El tenant debe formar parte de cada función:
TypeScript
Copiar async function findOrderById (
businessId: ObjectId,
orderId: ObjectId
) : Promise < Order | null > {
return orders. findOne ( {
_id: orderId,
businessId
} ) ;
} No busques por _id y verifiques tenant después. El filtro debe impedir acceder al documento desde el inicio.
La repository define filtro, sort, projection y limit:
TypeScript
Copiar return orders. find (
{
businessId,
status
} ,
{
projection: {
status: 1 ,
total: 1 ,
createdAt: 1
}
}
)
. sort ( { createdAt: - 1 , _id: - 1 } )
. limit ( limit)
. toArray ( ) ; El controller solo entrega parámetros validados.
TypeScript
Copiar async function confirmOrder ( input: {
businessId: ObjectId;
orderId: ObjectId;
expectedVersion: number ;
session? : ClientSession;
} ) {
const result = await orders. findOneAndUpdate (
{
_id: input. orderId,
businessId: input. businessId,
status: 'pending' ,
version: input. expectedVersion
} ,
{
$set: {
status: 'confirmed' ,
confirmedAt: new Date ( )
} ,
$inc: { version: 1 }
} ,
{
returnDocument: 'after' ,
session: input. session
}
) ;
if ( ! result) {
throw new OrderStateConflictError ( ) ;
}
return mapOrder ( result) ;
} La repository encapsula la precondition y traduce ausencia a un error de dominio.
Toda función que pueda participar en una transacción recibe session opcional:
TypeScript
Copiar session? : ClientSessionLa propaga a cada operación. No debe crear una session nueva internamente si el caso de uso ya controla la transacción.
Separa documento persistido de dominio y respuesta:
TypeScript
Copiar function mapOrder ( document: OrderDocument) : Order {
return {
id: document. _id. toString ( ) ,
total: document. total. toString ( ) ,
status: document. status,
createdAt: document. createdAt
} ;
} Así evitas que ObjectId, Decimal128, __v o campos internos se filtren a otras capas.
La repository traduce errores conocidos:
duplicate key → conflicto específico;
schema validation → dato inválido;
matchedCount cero → not found o precondition failed;
timeout/network → error de infraestructura;
unknown commit result → protocolo de retry.
No conviertas todos los errores en null. Conserva cause para observabilidad.
Implementa cursor estable, no skip profundo:
TypeScript
Copiar {
businessId,
$or: [
{ createdAt: { $lt: cursor. createdAt } } ,
{
createdAt: cursor. createdAt,
_id: { $lt: cursor. id }
}
]
} La repository debe codificar y validar el cursor, y el índice debe coincidir con el sort.
Las aggregations pertenecen a funciones con nombre:
TypeScript
Copiar getDailySalesSummary ( { businessId, from, to } ) No expongas pipelines a controllers. La repository controla stages, límites, tenant y tipos de salida.
La capa puede usar cualquiera. El contrato externo no debería depender de hydrated documents, Filter<T> ni métodos del ODM.
Esto facilita cambiar implementation parcial sin afirmar que toda la base es intercambiable.
El caso de uso decide la unidad:
Texto
Copiar service
→ inicia session/transaction
→ repository A(session)
→ repository B(session)
→ commitLa repository no debe ejecutar HTTP, emails ni efectos externos dentro de la transacción.
Prueba repositories contra MongoDB real:
índices;
ObjectId y Decimal128;
duplicate keys;
tenant isolation;
pagination;
preconditions;
transactions;
aggregations;
schema versions.
Mocks pueden probar el service, pero no demuestran que una query funcione.
Añade comment o metadata de query shape cuando sea compatible. Mide duración y resultados sin registrar filtros sensibles.
Nombres como orders:list-pending:v2 facilitan relacionar profiler y código.
En sistemas con múltiples fuentes, la repository puede traducir formatos externos al dominio. Pero no debe convertirse en una clase gigante que mezcla reglas de negocio, cache, HTTP y persistencia.
session omitida en una operación;
query por ID sin tenant;
DTO pierde precisión Decimal128;
generic repository permite $where;
cursor con tipo inválido;
aggregate devuelve null inesperado;
error duplicate key se asigna a la regla equivocada;
repository oculta una query muy costosa.
repositories genéricas para todo;
exponer Filter<T> al controller;
devolver documentos Mongoose;
crear una transacción por método;
olvidar projection y limit;
mezclar autorización después de leer;
traducir todos los errores a null;
no probar contra MongoDB real;
esconder query shapes críticas.
Revisa firmas públicas.
Busca filtros construidos desde requests.
Prueba tenant isolation.
Ejecuta transaction con session.
Comprueba DTOs BSON.
Prueba conflictos concurrentes.
Ejecuta explain de hot paths.
Revisa pagination.
Instrumenta query comments.
Elimina abstracciones que impidan usar MongoDB correctamente.
Una repository útil protege contratos y hace explícitas las operaciones del dominio. Debe centralizar tenant, queries, sessions, DTOs y errores sin reducir MongoDB a CRUD. Las funciones con intención son más seguras y mantenibles que una abstracción genérica que deja pasar filtros arbitrarios.
Comprueba lo aprendido
¿Por qué evitar un generic repository?
¿Dónde debe aplicarse el tenant?
¿Quién inicia una transacción?
¿Qué traduce el mapper?
¿Cómo modelarías cursor pagination?
¿Qué debes probar con MongoDB real?