Skip to content

Repository Pattern

The Repository pattern provides a clean separation between the domain model and data access logic. It allows domain objects to be persisted and retrieved without exposing the underlying infrastructure.

Current State

The current implementation uses a BaseRepository class that all repositories extend:

JavaScript
// src/infra/repositories/baseRepository.js
class BaseRepository {
  constructor(model) {
    this.tableName = model;
  }

  async create(payload, options = {}) {
    const result = await this.tableName.create(payload, {
      ...options,
      userId: options.userId,
      payload,
    });

    return result.toJSON();
  }

  async update(where, payload, options = {}) {
    const result = await this.tableName.update(payload, {
      where,
      ...options,
      individualHooks: true,
      userId: options.userId,
      payload,
    });

    return result;
  }

  // ... other methods
}

Issues with the current implementation:

  1. Domain entities are directly mapped to database models
  2. No clear separation between domain and persistence concerns
  3. Domain layer has knowledge of infrastructure details
  4. No repository interfaces in the domain layer

Improved Repository Pattern

1. Define Repository Interfaces in the Domain Layer

Repository interfaces should be defined in the domain layer to enforce the Dependency Inversion Principle:

JavaScript
// src/domain/requisitionManagement/repositories/RequisitionRepository.js
class RequisitionRepository {
  /**
   * Find a requisition by its ID
   * @param {number} id - The requisition ID
   * @returns {Promise<Requisition|null>} - The requisition or null if not found
   */
  async findById(id) {
    throw new Error('Method not implemented');
  }

  /**
   * Save a requisition (create or update)
   * @param {Requisition} requisition - The requisition to save
   * @returns {Promise<Requisition>} - The saved requisition
   */
  async save(requisition) {
    throw new Error('Method not implemented');
  }

  /**
   * Find requisitions by status
   * @param {string} status - The status to filter by
   * @param {Object} options - Additional options (pagination, etc.)
   * @returns {Promise<{data: Requisition[], total: number}>} - The requisitions and total count
   */
  async findByStatus(status, options = {}) {
    throw new Error('Method not implemented');
  }

  /**
   * Find requisitions by creator
   * @param {number} userId - The user ID
   * @param {Object} options - Additional options (pagination, etc.)
   * @returns {Promise<{data: Requisition[], total: number}>} - The requisitions and total count
   */
  async findByCreator(userId, options = {}) {
    throw new Error('Method not implemented');
  }

  /**
   * Delete a requisition
   * @param {number} id - The requisition ID
   * @returns {Promise<boolean>} - True if deleted, false otherwise
   */
  async delete(id) {
    throw new Error('Method not implemented');
  }
}

module.exports = RequisitionRepository;

2. Implement Repository in the Infrastructure Layer

JavaScript
// src/infrastructure/repositories/SequelizeRequisitionRepository.js
const RequisitionRepository = require('../../domain/requisitionManagement/repositories/RequisitionRepository');
const Requisition = require('../../domain/requisitionManagement/entities/Requisition');
const RequisitionItem = require('../../domain/requisitionManagement/entities/RequisitionItem');
const RequisitionApprover = require('../../domain/requisitionManagement/entities/RequisitionApprover');

class SequelizeRequisitionRepository extends RequisitionRepository {
  constructor(models, eventBus) {
    super();
    this.requisitionModel = models.requisitionModel;
    this.requisitionItemModel = models.requisitionItemListModel;
    this.requisitionApproverModel = models.requisitionApproverModel;
    this.eventBus = eventBus;
  }

  async findById(id) {
    const requisitionData = await this.requisitionModel.findByPk(id, {
      include: [
        {
          model: this.requisitionItemModel,
          as: 'requisitionItemLists',
        },
        {
          model: this.requisitionApproverModel,
          as: 'requisitionApprovers',
        },
      ],
    });

    if (!requisitionData) {
      return null;
    }

    return this._toDomain(requisitionData);
  }

  async save(requisition) {
    const transaction = await this.requisitionModel.sequelize.transaction();

    try {
      const requisitionData = this._toPersistence(requisition);

      let savedRequisition;

      if (requisition.id) {
        // Update existing requisition
        await this.requisitionModel.update(
          requisitionData,
          {
            where: { id: requisition.id },
            transaction,
          }
        );

        // Update items
        for (const item of requisition.items) {
          if (item.id) {
            await this.requisitionItemModel.update(
              this._itemToPersistence(item),
              {
                where: { id: item.id },
                transaction,
              }
            );
          } else {
            const savedItem = await this.requisitionItemModel.create(
              {
                ...this._itemToPersistence(item),
                requisitionId: requisition.id,
              },
              { transaction }
            );
            item.id = savedItem.id;
          }
        }

        // Update approvers
        for (const approver of requisition.approvers) {
          if (approver.id) {
            await this.requisitionApproverModel.update(
              this._approverToPersistence(approver),
              {
                where: { id: approver.id },
                transaction,
              }
            );
          } else {
            const savedApprover = await this.requisitionApproverModel.create(
              {
                ...this._approverToPersistence(approver),
                requisitionId: requisition.id,
              },
              { transaction }
            );
            approver.id = savedApprover.id;
          }
        }

        savedRequisition = await this.requisitionModel.findByPk(
          requisition.id,
          {
            include: [
              {
                model: this.requisitionItemModel,
                as: 'requisitionItemLists',
              },
              {
                model: this.requisitionApproverModel,
                as: 'requisitionApprovers',
              },
            ],
            transaction,
          }
        );
      } else {
        // Create new requisition
        savedRequisition = await this.requisitionModel.create(
          requisitionData,
          { transaction }
        );

        requisition.id = savedRequisition.id;

        // Create items
        for (const item of requisition.items) {
          const savedItem = await this.requisitionItemModel.create(
            {
              ...this._itemToPersistence(item),
              requisitionId: requisition.id,
            },
            { transaction }
          );
          item.id = savedItem.id;
        }

        // Create approvers
        for (const approver of requisition.approvers) {
          const savedApprover = await this.requisitionApproverModel.create(
            {
              ...this._approverToPersistence(approver),
              requisitionId: requisition.id,
            },
            { transaction }
          );
          approver.id = savedApprover.id;
        }

        savedRequisition = await this.requisitionModel.findByPk(
          requisition.id,
          {
            include: [
              {
                model: this.requisitionItemModel,
                as: 'requisitionItemLists',
              },
              {
                model: this.requisitionApproverModel,
                as: 'requisitionApprovers',
              },
            ],
            transaction,
          }
        );
      }

      await transaction.commit();

      return this._toDomain(savedRequisition);
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }

  // Private methods to map between domain and persistence
  _toDomain(data) {
    const items = data.requisitionItemLists?.map(item => 
      new RequisitionItem({
        id: item.id,
        itemId: item.itemId,
        quantity: item.quantity,
        unit: item.unit,
        description: item.description,
      })
    ) || [];

    const approvers = data.requisitionApprovers?.map(approver => 
      new RequisitionApprover({
        id: approver.id,
        userId: approver.userId,
        status: approver.status,
        level: approver.level,
      })
    ) || [];

    return Requisition.reconstitute({
      id: data.id,
      rsNumber: data.rsNumber,
      rsLetter: data.rsLetter,
      companyCode: data.companyCode,
      createdBy: data.createdBy,
      companyId: data.companyId,
      departmentId: data.departmentId,
      projectId: data.projectId,
      dateRequired: data.dateRequired,
      deliveryAddress: data.deliveryAddress,
      purpose: data.purpose,
      status: data.status,
      type: data.type,
      items,
      approvers,
    });
  }

  _toPersistence(domain) {
    return {
      id: domain.id,
      rsNumber: domain.rsNumber,
      rsLetter: domain.rsLetter,
      companyCode: domain.companyCode,
      createdBy: domain.createdBy,
      companyId: domain.companyId,
      departmentId: domain.departmentId,
      projectId: domain.projectId,
      dateRequired: domain.dateRequired,
      deliveryAddress: domain.deliveryAddress,
      purpose: domain.purpose,
      status: domain.status.value,
      type: domain.type,
    };
  }

  _itemToPersistence(item) {
    return {
      id: item.id,
      itemId: item.itemId,
      quantity: item.quantity,
      unit: item.unit,
      description: item.description,
    };
  }

  _approverToPersistence(approver) {
    return {
      id: approver.id,
      userId: approver.userId,
      status: approver.status,
      level: approver.level,
    };
  }
}

module.exports = SequelizeRequisitionRepository;