Backend

Repositories

The repository pattern — data access with MongoDB and Redis caching.

Repositories are the only layer that touches MongoDB and Redis. Every collection has a dedicated repository built on a generic useRepo base.

Base Repository — useRepo

src/repositories/base.repo.ts provides generic CRUD operations with built-in Redis cache management:

// src/repositories/base.repo.ts (simplified)
import { useCache } from '@codisolutions23/node-utils'
import { useAtlas } from '@codisolutions23/node-utils'
import { paginate } from '@codisolutions23/node-utils'

export function useRepo<T>(collectionName: string) {
  const { getDb }               = useAtlas()
  const { getCache, setCache, delCacheGroup } = useCache()

  function col() {
    return getDb().collection<T>(collectionName)
  }

  async function add(doc: T, session?: ClientSession) {
    const result = await col().insertOne(doc as any, { session })
    await delCacheGroup(collectionName)     // invalidate on write
    return { ...doc, _id: result.insertedId }
  }

  async function getAll(
    pipeline: Document[],
    page: number,
    limit: number,
    cacheOptions?: { key: string; ttl: number },
  ) {
    if (cacheOptions) {
      const cached = await getCache(cacheOptions.key)
      if (cached) return cached
    }

    const [data, count] = await Promise.all([
      col().aggregate([...pipeline, { $skip: (page - 1) * limit }, { $limit: limit }]).toArray(),
      col().aggregate([...pipeline, { $count: 'total' }]).toArray(),
    ])

    const result = paginate(data, count[0]?.total ?? 0, page, limit)

    if (cacheOptions) await setCache(cacheOptions.key, result, cacheOptions.ttl)

    return result
  }

  async function getById(id: string, pipeline?: Document[]) {
    // ...
  }

  async function update(id: string, update: Partial<T>, session?: ClientSession) {
    await col().updateOne(
      { _id: new ObjectId(id) } as any,
      { $set: { ...update, updatedAt: new Date() } },
      { session },
    )
    await delCacheGroup(collectionName)
    return getById(id)
  }

  async function softDelete(id: string) {
    return update(id, { deletedAt: new Date() } as any)
  }

  return { add, getAll, getById, update, softDelete, col }
}

Specific Repository

Each collection wraps the base repo with typed, domain-specific query methods:

// src/repositories/user.repo.ts
import { useRepo } from './base.repo'
import { buildCacheKey } from '@codisolutions23/node-utils'
import { CollectionName } from '../enums/collection.enum'
import type { IUser, TUserCreate } from '../models/user.model'
import { User } from '../models/user.model'

export function useUserRepo() {
  const base = useRepo<IUser>(CollectionName.USERS)

  function createUser(value: TUserCreate, session?: ClientSession) {
    return base.add(new User(value), session)
  }

  function getUserByEmail(email: string, organizationId?: string) {
    const pipeline = [
      { $match: { email, deletedAt: null, ...(organizationId ? { organizationId: new ObjectId(organizationId) } : {}) } },
    ]
    return base.col().aggregate<IUser>(pipeline).next()
  }

  function getUsers({ organizationId, page = 1, limit = 10 }: { organizationId: string; page?: number; limit?: number }) {
    const cacheKey = buildCacheKey(CollectionName.USERS, { organizationId, page, limit })
    const pipeline = [
      { $match: { organizationId: new ObjectId(organizationId), deletedAt: null } },
      { $sort: { createdAt: -1 } },
    ]
    return base.getAll(pipeline, page, limit, { key: cacheKey, ttl: 300 })
  }

  function updateUser(id: string, update: Partial<IUser>, session?: ClientSession) {
    return base.update(id, update, session)
  }

  function deleteUser(id: string) {
    return base.softDelete(id)
  }

  return { createUser, getUserByEmail, getUsers, updateUser, deleteUser }
}

Rules

  • Never query MongoDB outside a repository. No getDb() calls in controllers or services.
  • Always use softDelete (sets deletedAt) — never hard-delete unless absolutely required.
  • Cache reads — pass a cacheKey (via buildCacheKey) and ttl to getAll/getById.
  • Invalidate on write — the base repo calls delCacheGroup(collectionName) automatically on add/update/softDelete.
  • Aggregate pipelines for complex queries — not findOne + in-memory filtering.